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:
-
Go to Products and create two products:
- Starter (monthly) — e.g., $29/month
- Pro (monthly or yearly) — e.g., $297/year
-
For each Price, record the Price ID (starts with
price_):- Set
STRIPE_STARTER_PRICE_IDfor the Starter price - Set
STRIPE_PRO_PRICE_IDfor the Pro price
- Set
-
Add metadata to each Price:
credits_per_period: Number of AI credits granted each billing period (e.g.,1000for Starter,10000for 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:
- Click Add endpoint.
- Set the URL to your deployed Firebase Functions
stripeWebhookendpoint:https://us-central1-<project-id>.cloudfunctions.net/stripeWebhook - Select these events to send:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.subscription.trial_will_endinvoice.payment_succeededinvoice.payment_failed
- Copy the signing secret and set it as
STRIPE_WEBHOOK_SECRETin both your web app and functions environment.
4. Test the integration
- Start the app:
pnpm dev - Open http://localhost:3000
- Navigate to the pricing page and start a free trial
- Use Stripe test card:
4242 4242 4242 4242(any future expiry, any CVC) - After checkout, you should be redirected to the dashboard with an active trial
How it works
Checkout flow
- User selects a plan — Frontend calls
POST /api/create-checkout-sessionwith{ planId } - Stripe Checkout — User completes payment on Stripe's hosted page
- Webhook fires —
checkout.session.completedwrites temp data totempSubscriptions/{customerId} - Session verification — Frontend calls
POST /api/verify-sessionwith{ sessionId }to finalize the subscription - Firestore updated — Subscription doc written to
users/{uid}/subscription/current - Credits allocated — Initial credits granted based on plan's
credits_per_periodmetadata
Billing portal
Users manage their subscription (cancel, change card, view invoices) through Stripe's billing portal:
- Frontend calls
POST /api/billing/portal(requires__sessioncookie) - Redirects to Stripe's hosted portal
Plan changes
Instant plan upgrades are supported:
- Frontend calls
POST /api/billing/change-planwith{ planId }+ Firebase ID token - If payment confirmation is needed, the response includes
requiresActionandpaymentIntentClientSecret - New plan's credit allocation applies at the next billing period (stored as
pendingPlan)
Credits lifecycle
| Event | Action |
|---|---|
customer.subscription.created | Grant initial credits for the current period |
invoice.payment_succeeded | Top up credits for the new billing period |
customer.subscription.updated | Store pendingPlan for next-period changes |
customer.subscription.deleted | No 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_KEYorSTRIPE_WEBHOOK_SECRETto the client - Firestore security rules protect subscription data (Admin SDK writes only)
- The
processedSessions/{sessionId}collection prevents duplicate subscription processing
Related docs
- Environment & Tooling — All environment variables
- Subscriptions — Detailed subscription flow and credit system
- Vercel Environment Variables — Production deployment config
- Firestore Data Model — Complete database schema