Skip to main content

Subscriptions

Quick links: Frontend β€’ Backend

Frontend​

  • Start checkout: call POST /api/create-checkout-session with { 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-session with { sessionId } to finalize and receive { subscription, customToken }.
  • Billing portal: call POST /api/billing/portal (requires __session cookie) and redirect to returned { url }.
  • Instant plan upgrade: call POST /api/billing/change-plan with { planId } plus Firebase ID token (Authorization header). If the response includes requiresAction and paymentIntentClientSecret, confirm via Stripe.js before refreshing claims.
  • Helpers in apps/web/src/lib/firestore.ts: hasActiveSubscription(), isInTrialPeriod().

Related backend: see Backend

Backend​

Flow:

  1. POST /api/create-checkout-session creates Stripe Checkout with a trial and embeds metadata for reconciliation.
  2. Webhook checkout.session.completed writes temp data keyed by customer.id under tempSubscriptions/{customerId}.
  3. POST /api/verify-session uses the session_id to look up the session, find/create the user, link stripeCustomerId, and upsert users/{uid}/subscription/current.
  4. POST /api/billing/portal creates 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 use subscription.current_period_start/end.
  • On customer.subscription.updated (plan change):
    • MVP: new plan applies next period; users/{uid}/subscription/current.pendingPlan records { 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).
  • On customer.subscription.deleted: no further top‑ups; remaining balance lasts until periodEnd.

API enforcement​

  • Public API routes (/v1/posts, /v1/media) enforce active subscription in the api Function.
  • 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