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 fires — checkout.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