Skip to main content

Stripe Setup

Soku uses Stripe for subscription management with free trials, tiered plans, webhook-driven lifecycle events, and a credit system for AI features.


What's included

  • 7-day free trial on all plans
  • Tiered subscriptions: Starter and Pro plans with different credit allowances
  • Credit system: AI features (transcription, caption generation) consume credits allocated per billing period
  • Webhook handling for lifecycle events (created, updated, deleted, trial ending, invoice events)
  • Subscription-gated routes — dashboard and API access require active subscription
  • Billing portal — users can manage their subscription through Stripe's hosted portal

1. Create products and prices

In the Stripe Dashboard:

  1. Go to Products and create two products:

    • Starter (monthly) — e.g., $29/month
    • Pro (monthly or yearly) — e.g., $297/year
  2. For each Price, record the Price ID (starts with price_):

    • Set STRIPE_STARTER_PRICE_ID for the Starter price
    • Set STRIPE_PRO_PRICE_ID for the Pro price
  3. Add metadata to each Price:

    • credits_per_period: Number of AI credits granted each billing period (e.g., 1000 for Starter, 10000 for Pro)

2. Configure environment variables

In apps/web/.env.local:

STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_STARTER_PRICE_ID=price_...
STRIPE_PRO_PRICE_ID=price_...

For production deployment on Vercel, add these same variables in Vercel Environment Variables.


3. Set up the webhook endpoint

In Stripe Dashboard, go to Developers > Webhooks:

  1. Click Add endpoint.
  2. Set the URL to your deployed Firebase Functions stripeWebhook endpoint:
    https://us-central1-<project-id>.cloudfunctions.net/stripeWebhook
  3. Select these events to send:
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • customer.subscription.trial_will_end
    • invoice.payment_succeeded
    • invoice.payment_failed
  4. Copy the signing secret and set it as STRIPE_WEBHOOK_SECRET in both your web app and functions environment.

4. Test the integration

  1. Start the app: pnpm dev
  2. Open http://localhost:3000
  3. Navigate to the pricing page and start a free trial
  4. Use Stripe test card: 4242 4242 4242 4242 (any future expiry, any CVC)
  5. After checkout, you should be redirected to the dashboard with an active trial

How it works

Checkout flow

  1. User selects a plan — Frontend calls POST /api/create-checkout-session with { planId }
  2. Stripe Checkout — User completes payment on Stripe's hosted page
  3. Webhook firescheckout.session.completed writes temp data to tempSubscriptions/{customerId}
  4. Session verification — Frontend calls POST /api/verify-session with { sessionId } to finalize the subscription
  5. Firestore updated — Subscription doc written to users/{uid}/subscription/current
  6. Credits allocated — Initial credits granted based on plan's credits_per_period metadata

Billing portal

Users manage their subscription (cancel, change card, view invoices) through Stripe's billing portal:

  • Frontend calls POST /api/billing/portal (requires __session cookie)
  • Redirects to Stripe's hosted portal

Plan changes

Instant plan upgrades are supported:

  • Frontend calls POST /api/billing/change-plan with { planId } + Firebase ID token
  • If payment confirmation is needed, the response includes requiresAction and paymentIntentClientSecret
  • New plan's credit allocation applies at the next billing period (stored as pendingPlan)

Credits lifecycle

EventAction
customer.subscription.createdGrant initial credits for the current period
invoice.payment_succeededTop up credits for the new billing period
customer.subscription.updatedStore pendingPlan for next-period changes
customer.subscription.deletedNo further top-ups; balance lasts until periodEnd

All credit operations are logged to users/{uid}/creditLedger/{entryId} for audit and dispute resolution. Ledger entries are immutable and use idempotencyKey to prevent duplicate charges.

API enforcement

Public API routes (/v1/posts, /v1/media, etc.) enforce active subscription in the middleware chain:

authenticateApiKey → requireActiveSubscription → route handler

Allowed subscription states: trialing, active (with currentPeriodEnd > now). Inactive or expired subscriptions receive 403 forbidden.


Firestore schema

users/{uid}/subscription/current
{
customerId: "cus_...",
subscriptionId: "sub_...",
status: "trialing" | "active" | "canceled" | "past_due" | "unpaid",
planId: "starter" | "pro",
priceId: "price_...",
currentPeriodStart: timestamp,
currentPeriodEnd: timestamp,
trialEnd: timestamp | null,
cancelAtPeriodEnd: boolean,
pendingPlan?: { priceId, planId, creditsPerPeriod, appliesAt }
}

See Firestore Data Model for the complete schema.


Security notes

  • Webhook signature verification is required and implemented in webhooks/stripe.js
  • Secret keys are server-side only — never expose STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET to the client
  • Firestore security rules protect subscription data (Admin SDK writes only)
  • The processedSessions/{sessionId} collection prevents duplicate subscription processing