# User Subscription System - Implementation Plan ## Overview Implement a subscription-based model for Biblical Guide that limits AI chat conversations for free users and offers paid tiers with increased or unlimited access. ## Current State Analysis ### What EXISTS ✅ - Authentication system (JWT-based, required for chat) - Chat conversation tracking in database - User model with basic fields - Stripe integration for one-time donations - Stripe webhook handling (donations only) ### What DOES NOT EXIST ❌ - User subscription system - Subscription tiers/plans - Conversation limits (free vs paid) - Usage tracking/quota enforcement - Upgrade prompts when limits reached - Subscription management UI - Stripe subscription integration (only donations exist) ## Subscription Tiers ### Free Tier - **Price:** $0/month - **Conversations:** 10 per month - **Features:** - Full Bible access - Prayer wall access - Bookmarks & highlights - 10 AI conversations/month - **Reset:** Monthly on signup anniversary ### Premium Tier - **Price:** $10/month (or $100/year with 17% discount) - **Conversations:** Unlimited - **Features:** - Everything in Free - Unlimited AI conversations - Priority support - Early access to new features ### Donation System (Existing) - One-time donations (separate from subscriptions) - Recurring donations (separate from subscriptions) - No perks attached to donations ## Implementation Phases --- ## Phase 1: Database Schema & Migrations ### 1.1 Update User Model **File:** `prisma/schema.prisma` Add to User model: ```prisma model User { // ... existing fields ... // Subscription fields subscriptionTier String @default("free") // "free", "premium" subscriptionStatus String @default("active") // "active", "cancelled", "expired", "past_due" conversationLimit Int @default(10) conversationCount Int @default(0) // Reset monthly limitResetDate DateTime? // When to reset conversation count stripeCustomerId String? @unique // For subscriptions (not donations) stripeSubscriptionId String? @unique // Relations subscriptions Subscription[] } ``` ### 1.2 Create Subscription Model ```prisma model Subscription { id String @id @default(uuid()) userId String stripeSubscriptionId String @unique stripePriceId String // Stripe price ID for the plan stripeCustomerId String status SubscriptionStatus currentPeriodStart DateTime currentPeriodEnd DateTime cancelAtPeriodEnd Boolean @default(false) tier String // "premium" interval String // "month" or "year" metadata Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([status]) @@index([stripeSubscriptionId]) } enum SubscriptionStatus { ACTIVE CANCELLED PAST_DUE TRIALING INCOMPLETE INCOMPLETE_EXPIRED UNPAID } ``` ### 1.3 Run Migration ```bash npx prisma migrate dev --name add_subscription_system npx prisma generate ``` --- ## Phase 2: Conversation Limit Enforcement ### 2.1 Update Chat API Route **File:** `app/api/chat/route.ts` Add conversation limit check before processing: ```typescript // Add after authentication check (line 58) // Check conversation limits for authenticated users if (userId && !conversationId) { // Only check limits when creating NEW conversation const user = await prisma.user.findUnique({ where: { id: userId }, select: { subscriptionTier: true, conversationCount: true, conversationLimit: true, limitResetDate: true, subscriptionStatus: true } }) if (!user) { return NextResponse.json({ success: false, error: 'User not found', code: 'USER_NOT_FOUND' }, { status: 404 }) } // Reset counter if period expired const now = new Date() if (user.limitResetDate && now > user.limitResetDate) { // Reset monthly counter const nextResetDate = new Date(user.limitResetDate) nextResetDate.setMonth(nextResetDate.getMonth() + 1) await prisma.user.update({ where: { id: userId }, data: { conversationCount: 0, limitResetDate: nextResetDate } }) user.conversationCount = 0 } // Check if user has exceeded limit (only for free tier with active status) if (user.subscriptionTier === 'free' && user.subscriptionStatus === 'active') { if (user.conversationCount >= user.conversationLimit) { return NextResponse.json({ success: false, error: 'Conversation limit reached. Upgrade to Premium for unlimited conversations.', code: 'LIMIT_REACHED', data: { limit: user.conversationLimit, used: user.conversationCount, tier: user.subscriptionTier, upgradeUrl: `/${locale}/subscription` } }, { status: 403 }) } } // User is within limits - increment counter for new conversations await prisma.user.update({ where: { id: userId }, data: { conversationCount: { increment: 1 }, // Set initial reset date if not set limitResetDate: user.limitResetDate || new Date(now.setMonth(now.getMonth() + 1)) } }) } ``` ### 2.2 Create Utility Functions **File:** `lib/subscription-utils.ts` (NEW) ```typescript import { PrismaClient } from '@prisma/client' const prisma = new PrismaClient() export const SUBSCRIPTION_LIMITS = { free: 10, premium: Infinity } export const STRIPE_PRICES = { premium_monthly: process.env.STRIPE_PREMIUM_MONTHLY_PRICE_ID!, premium_yearly: process.env.STRIPE_PREMIUM_YEARLY_PRICE_ID! } export async function checkConversationLimit(userId: string): Promise<{ allowed: boolean remaining: number limit: number tier: string }> { const user = await prisma.user.findUnique({ where: { id: userId }, select: { subscriptionTier: true, conversationCount: true, conversationLimit: true, limitResetDate: true } }) if (!user) { throw new Error('User not found') } // Reset if needed const now = new Date() if (user.limitResetDate && now > user.limitResetDate) { const nextReset = new Date(user.limitResetDate) nextReset.setMonth(nextReset.getMonth() + 1) await prisma.user.update({ where: { id: userId }, data: { conversationCount: 0, limitResetDate: nextReset } }) user.conversationCount = 0 } const remaining = user.conversationLimit - user.conversationCount const allowed = user.subscriptionTier === 'premium' || remaining > 0 return { allowed, remaining: user.subscriptionTier === 'premium' ? Infinity : remaining, limit: user.conversationLimit, tier: user.subscriptionTier } } export function getTierFromPriceId(priceId: string): string { if (priceId === STRIPE_PRICES.premium_monthly || priceId === STRIPE_PRICES.premium_yearly) { return 'premium' } return 'free' } export function getIntervalFromPriceId(priceId: string): string { if (priceId === STRIPE_PRICES.premium_yearly) return 'year' return 'month' } ``` --- ## Phase 3: Stripe Subscription Integration ### 3.1 Create Stripe Products & Prices **Manual Step - Stripe Dashboard:** 1. Go to Stripe Dashboard → Products 2. Create product: "Biblical Guide Premium" 3. Add prices: - Monthly: $10/month (ID: save to env as `STRIPE_PREMIUM_MONTHLY_PRICE_ID`) - Yearly: $100/year (ID: save to env as `STRIPE_PREMIUM_YEARLY_PRICE_ID`) ### 3.2 Update Environment Variables **File:** `.env.local` and `.env.example` Add: ```env # Stripe Subscription Price IDs STRIPE_PREMIUM_MONTHLY_PRICE_ID=price_xxxxxxxxxxxxx STRIPE_PREMIUM_YEARLY_PRICE_ID=price_xxxxxxxxxxxxx ``` ### 3.3 Create Subscription Checkout API **File:** `app/api/subscriptions/checkout/route.ts` (NEW) ```typescript import { NextResponse } from 'next/server' import { z } from 'zod' import { stripe } from '@/lib/stripe' import prisma from '@/lib/db' import { verifyToken } from '@/lib/auth' const checkoutSchema = z.object({ priceId: z.string(), interval: z.enum(['month', 'year']), locale: z.string().default('en') }) export async function POST(request: Request) { try { // Verify authentication const authHeader = request.headers.get('authorization') if (!authHeader?.startsWith('Bearer ')) { return NextResponse.json( { success: false, error: 'Authentication required' }, { status: 401 } ) } const token = authHeader.substring(7) const payload = await verifyToken(token) const userId = payload.userId // Get user const user = await prisma.user.findUnique({ where: { id: userId }, select: { email: true, stripeCustomerId: true, subscriptionTier: true } }) if (!user) { return NextResponse.json( { success: false, error: 'User not found' }, { status: 404 } ) } // Check if already premium if (user.subscriptionTier === 'premium') { return NextResponse.json( { success: false, error: 'Already subscribed to Premium' }, { status: 400 } ) } const body = await request.json() const { priceId, interval, locale } = checkoutSchema.parse(body) // Create or retrieve Stripe customer let customerId = user.stripeCustomerId if (!customerId) { const customer = await stripe.customers.create({ email: user.email, metadata: { userId } }) customerId = customer.id await prisma.user.update({ where: { id: userId }, data: { stripeCustomerId: customerId } }) } // Create checkout session const session = await stripe.checkout.sessions.create({ customer: customerId, mode: 'subscription', payment_method_types: ['card'], line_items: [ { price: priceId, quantity: 1 } ], success_url: `${process.env.NEXTAUTH_URL}/${locale}/subscription/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${process.env.NEXTAUTH_URL}/${locale}/subscription`, metadata: { userId, interval }, subscription_data: { metadata: { userId } } }) return NextResponse.json({ success: true, sessionId: session.id, url: session.url }) } catch (error) { console.error('Subscription checkout error:', error) return NextResponse.json( { success: false, error: 'Failed to create checkout session' }, { status: 500 } ) } } ``` ### 3.4 Create Customer Portal API **File:** `app/api/subscriptions/portal/route.ts` (NEW) ```typescript import { NextResponse } from 'next/server' import { stripe } from '@/lib/stripe' import prisma from '@/lib/db' import { verifyToken } from '@/lib/auth' export async function POST(request: Request) { try { const authHeader = request.headers.get('authorization') if (!authHeader?.startsWith('Bearer ')) { return NextResponse.json( { success: false, error: 'Authentication required' }, { status: 401 } ) } const token = authHeader.substring(7) const payload = await verifyToken(token) const userId = payload.userId const user = await prisma.user.findUnique({ where: { id: userId }, select: { stripeCustomerId: true } }) if (!user?.stripeCustomerId) { return NextResponse.json( { success: false, error: 'No subscription found' }, { status: 404 } ) } const session = await stripe.billingPortal.sessions.create({ customer: user.stripeCustomerId, return_url: `${process.env.NEXTAUTH_URL}/settings` }) return NextResponse.json({ success: true, url: session.url }) } catch (error) { console.error('Customer portal error:', error) return NextResponse.json( { success: false, error: 'Failed to create portal session' }, { status: 500 } ) } } ``` ### 3.5 Update Stripe Webhook Handler **File:** `app/api/stripe/webhook/route.ts` Add new event handlers after existing donation handlers: ```typescript // After existing event handlers, add: case 'customer.subscription.created': case 'customer.subscription.updated': { const subscription = event.data.object const userId = subscription.metadata.userId if (!userId) { console.warn('No userId in subscription metadata') break } const priceId = subscription.items.data[0]?.price.id const tier = getTierFromPriceId(priceId) const interval = getIntervalFromPriceId(priceId) await prisma.subscription.upsert({ where: { stripeSubscriptionId: subscription.id }, create: { userId, stripeSubscriptionId: subscription.id, stripePriceId: priceId, stripeCustomerId: subscription.customer as string, status: subscription.status.toUpperCase(), currentPeriodStart: new Date(subscription.current_period_start * 1000), currentPeriodEnd: new Date(subscription.current_period_end * 1000), tier, interval }, update: { status: subscription.status.toUpperCase(), currentPeriodStart: new Date(subscription.current_period_start * 1000), currentPeriodEnd: new Date(subscription.current_period_end * 1000), cancelAtPeriodEnd: subscription.cancel_at_period_end } }) // Update user subscription tier await prisma.user.update({ where: { id: userId }, data: { subscriptionTier: tier, conversationLimit: tier === 'premium' ? 999999 : 10, subscriptionStatus: subscription.status, stripeSubscriptionId: subscription.id } }) console.log(`✅ Subscription ${subscription.status} for user ${userId}`) break } case 'customer.subscription.deleted': { const subscription = event.data.object const sub = await prisma.subscription.findUnique({ where: { stripeSubscriptionId: subscription.id }, select: { userId: true } }) if (sub) { // Downgrade to free tier await prisma.user.update({ where: { id: sub.userId }, data: { subscriptionTier: 'free', conversationLimit: 10, subscriptionStatus: 'cancelled' } }) await prisma.subscription.update({ where: { stripeSubscriptionId: subscription.id }, data: { status: 'CANCELLED' } }) console.log(`✅ Subscription cancelled for user ${sub.userId}`) } break } case 'invoice.payment_succeeded': { const invoice = event.data.object if (invoice.subscription) { console.log(`✅ Payment succeeded for subscription ${invoice.subscription}`) } break } case 'invoice.payment_failed': { const invoice = event.data.object if (invoice.subscription) { const subscription = await prisma.subscription.findUnique({ where: { stripeSubscriptionId: invoice.subscription as string } }) if (subscription) { await prisma.user.update({ where: { id: subscription.userId }, data: { subscriptionStatus: 'past_due' } }) console.warn(`⚠️ Payment failed for subscription ${invoice.subscription}`) } } break } ``` --- ## Phase 4: Frontend Implementation ### 4.1 Subscription Page **File:** `app/[locale]/subscription/page.tsx` (NEW) Full subscription management page with: - Current plan display - Usage stats (conversations used/remaining) - Upgrade options - Monthly/yearly toggle - Stripe checkout integration - Manage subscription button (portal link) ### 4.2 Upgrade Modal Component **File:** `components/subscription/upgrade-modal.tsx` (NEW) Modal shown when limit is reached: - Clear messaging about limit - Show current usage - Upgrade CTA - Pricing display ### 4.3 Usage Display Component **File:** `components/subscription/usage-display.tsx` (NEW) Shows in settings/profile: - Conversations used this month - Progress bar - Reset date - Current tier badge ### 4.4 Success Page **File:** `app/[locale]/subscription/success/page.tsx` (NEW) Thank you page after successful subscription ### 4.5 Update Settings Page **File:** `app/[locale]/settings/page.tsx` Add subscription section showing: - Current plan - Usage stats - Manage/upgrade buttons --- ## Phase 5: Translation Keys ### 5.1 Add to Translation Files **Files:** `messages/en.json`, `messages/ro.json`, `messages/es.json`, `messages/it.json` Add complete subscription translation keys: - Plan names and descriptions - Upgrade prompts - Usage messages - Error messages - Success messages --- ## Phase 6: Testing Checklist ### Subscription Flow - [ ] Free user creates 10 conversations successfully - [ ] 11th conversation blocked with upgrade prompt - [ ] Upgrade to Premium via Stripe Checkout - [ ] Webhook updates user to Premium tier - [ ] Premium user has unlimited conversations - [ ] Monthly counter resets correctly - [ ] Cancel subscription (remains premium until period end) - [ ] Subscription expires → downgrade to free - [ ] Payment failure handling ### Edge Cases - [ ] User with existing Stripe customer ID - [ ] Multiple subscriptions (should prevent) - [ ] Webhook arrives before user returns from checkout - [ ] Invalid webhook signatures - [ ] Database transaction failures - [ ] Subscription status edge cases (past_due, unpaid, etc.) --- ## Phase 7: Deployment ### 7.1 Environment Variables Add to production: ```env STRIPE_PREMIUM_MONTHLY_PRICE_ID=price_live_xxxxx STRIPE_PREMIUM_YEARLY_PRICE_ID=price_live_xxxxx ``` ### 7.2 Stripe Webhook Configuration Production webhook endpoint: - URL: `https://biblical-guide.com/api/stripe/webhook` - Events: - `customer.subscription.created` - `customer.subscription.updated` - `customer.subscription.deleted` - `invoice.payment_succeeded` - `invoice.payment_failed` - `checkout.session.completed` (existing) - `checkout.session.expired` (existing) ### 7.3 Database Migration Run in production: ```bash npx prisma migrate deploy ``` ### 7.4 Monitoring Set up monitoring for: - Subscription webhook failures - Payment failures - Limit enforcement errors - Subscription status inconsistencies --- ## Implementation Order 1. **Phase 1** - Database schema (30 min) 2. **Phase 2** - Conversation limits (1 hour) 3. **Phase 3** - Stripe subscription APIs (2 hours) 4. **Phase 4** - Frontend pages (3 hours) 5. **Phase 5** - Translations (1 hour) 6. **Phase 6** - Testing (2 hours) 7. **Phase 7** - Deployment (1 hour) **Total Estimated Time:** 10-12 hours --- ## Success Metrics ### Technical - ✅ All webhook events handled correctly - ✅ No conversation limit bypasses - ✅ Proper subscription status sync - ✅ Clean upgrade/downgrade flows ### Business - Track conversion rate: free → premium - Monitor churn rate - Track average subscription lifetime - Monitor support tickets related to limits --- ## Future Enhancements - Team/family plans - Annual discount improvements - Gift subscriptions - Free trial period for Premium - Referral program - Custom limits for special users - API access tier - Lifetime access option --- ## Notes - Keep donations separate from subscriptions - Donations do NOT grant subscription perks - Clear communication about free tier limits - Grace period for payment failures (3 days) - Prorated charges when upgrading mid-cycle - Email notifications for limit approaching - Email notifications for payment issues