Implement complete backend subscription system that limits free users to 10 AI conversations per month and offers Premium tier ($10/month or $100/year) with unlimited conversations. Changes: - Add User subscription fields (tier, status, limits, counters) - Create Subscription model to track Stripe subscriptions - Implement conversation limit enforcement in chat API - Add subscription checkout and customer portal APIs - Update Stripe webhook to handle subscription events - Add subscription utility functions (limit checks, tier management) - Add comprehensive subscription translations (en, ro, es, it) - Update environment variables for Stripe price IDs - Update footer "Sponsor Us" link to point to /donate - Add "Sponsor Us" button to home page hero section Database: - User model: subscriptionTier, subscriptionStatus, conversationLimit, conversationCount, limitResetDate, stripeCustomerId, stripeSubscriptionId - Subscription model: tracks Stripe subscription details, periods, status - SubscriptionStatus enum: ACTIVE, CANCELLED, PAST_DUE, TRIALING, etc. API Routes: - POST /api/subscriptions/checkout - Create Stripe checkout session - POST /api/subscriptions/portal - Get customer portal link - Webhook handlers for: customer.subscription.created/updated/deleted, invoice.payment_succeeded/failed Features: - Free tier: 10 conversations/month with automatic monthly reset - Premium tier: Unlimited conversations - Automatic limit enforcement before conversation creation - Returns LIMIT_REACHED error with upgrade URL when limit hit - Stripe Customer Portal integration for subscription management - Automatic tier upgrade/downgrade via webhooks Documentation: - SUBSCRIPTION_IMPLEMENTATION_PLAN.md - Complete implementation plan - SUBSCRIPTION_IMPLEMENTATION_STATUS.md - Current status and next steps Frontend UI still needed: subscription page, upgrade modal, usage display 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
774 lines
19 KiB
Markdown
774 lines
19 KiB
Markdown
# 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
|