Subscriptions
Quick links: Frontend β’ Backend
Frontendβ
- Start checkout: call
POST /api/create-checkout-sessionwith{ planId }(requires Firebase ID token with verified email); redirect to returned Checkout URL. The route reuses an existing Stripe customer (by stored ID or email) and only creates one if none exists. - After success redirect, call
POST /api/verify-sessionwith{ sessionId }to finalize and receive{ subscription, customToken }. - Billing portal: call
POST /api/billing/portal(requires__sessioncookie) and redirect to returned{ url }. - Instant plan upgrade: call
POST /api/billing/change-planwith{ planId }plus Firebase ID token (Authorization header). If the response includesrequiresActionandpaymentIntentClientSecret, confirm via Stripe.js before refreshing claims. - Helpers in
apps/web/src/lib/firestore.ts:hasActiveSubscription(),isInTrialPeriod().
Related backend: see Backend
Backendβ
Flow:
POST /api/create-checkout-sessioncreates Stripe Checkout with a trial and embeds metadata for reconciliation.- Webhook
checkout.session.completedwrites temp data keyed bycustomer.idundertempSubscriptions/{customerId}. POST /api/verify-sessionuses thesession_idto look up the session, find/create the user, linkstripeCustomerId, and upsertusers/{uid}/subscription/current.POST /api/billing/portalcreates a Stripe Billing Portal session for the authenticated user.
Credits lifecycleβ
- Stripe Price metadata includes
credits_per_period(numeric). Webhooks allocate credits per billing period based on this value. - On
customer.subscription.created: initial credits are granted for the subscriptionβs current period bounds. - On
invoice.payment_succeeded: credits are topped up for the new billing period. Period bounds usesubscription.current_period_start/end. - On
customer.subscription.updated(plan change):- MVP: new plan applies next period;
users/{uid}/subscription/current.pendingPlanrecords{ priceId, planId, creditsPerPeriod, appliesAt }. - Optional: if
CREDITS_PRORATE_UPGRADES=true, a proβrated topβup is applied immediately for upgrades (remaining fraction of period Γ credit delta).
- MVP: new plan applies next period;
- On
customer.subscription.deleted: no further topβups; remaining balance lasts untilperiodEnd.
API enforcementβ
- Public API routes (
/v1/posts,/v1/media) enforce active subscription in theapiFunction. - Middleware chain:
authenticateApiKeyβrequireActiveSubscriptionβ route handler. - Inactive or expired subscriptions receive
403 forbidden.
Data modelβ
users/{uid}/credits/current:{ balance, periodStart, periodEnd, planId, priceId, updatedAt }users/{uid}/creditLedger/{entryId}: immutable entries{ type, amount, source, idempotencyKey, createdAt }
Client reads GET /api/credits/balance to show remaining credits; server debits occur within Firestore transactions in server routes/functions.
Related frontend: see Frontend