Skip to content

Billing & Stripe Flow

This page explains how Workbench AI handles subscriptions and billing using Stripe + Supabase.

  • Stripe is the source of truth for billing events (subscriptions, invoices, payments).
  • Supabase stores a mirrored view of subscription status in the subscriptions and related tables.
  • The frontend coordinates checkout/upgrade/portal actions via Supabase Edge Functions.

Subscription lifecycle (high level)

From the app’s point of view, a tenant’s subscription goes through these stages:

  1. Signup + trial start

    • Owner chooses a plan on /pricing (e.g. starter, professional).
    • /signup creates a Supabase user + tenant/company record.
    • A Stripe Checkout Session is created via the create-checkout-session edge function.
    • Stripe manages any free trial periods configured on the Price.
  2. Checkout completed

    • User completes checkout on Stripe.
    • Stripe sends webhooks (e.g. checkout.session.completed, customer.subscription.created).
    • stripe-webhook edge function:
    • Validates the webhook using STRIPE_WEBHOOK_SECRET.
    • Finds the associated company_id (via metadata / subscription / customer lookups).
    • Upserts or updates a row in subscriptions with the new status and Stripe IDs.
    • Records the event in webhook_events_processed for idempotency.
  3. Ongoing subscription changes

    • Stripe events like customer.subscription.updated, invoice.paid, customer.subscription.deleted trigger stripe-webhook.
    • The webhook and sync helpers (sync-stripe-subscriptions, sync-payment-history) keep Supabase tables in sync.
    • The frontend reads subscription state via useSubscription and helpers like isSubscriptionActive.
  4. Upgrade/downgrade / plan changes

    • User initiates a plan change from /billing or via upgrade prompts.
    • Depending on UI design, either:
    • A new Stripe Checkout Session is created (again using create-checkout-session), or
    • The user is sent to the Stripe Billing Portal via create-portal-session.
    • Stripe adjusts subscription; webhooks update Supabase.
  5. Cancellation / reactivation

    • Cancellation may be triggered via Billing Portal or customer support.
    • Stripe webhooks update subscriptions status to canceled.
    • reactivate-subscription edge function can be used to restart a subscription using Stripe APIs and update Supabase.

Source of truth

Stripe is the authoritative source for billing. Supabase mirrors status for quick checks and UI gating. When in doubt, trust Stripe and let sync/webhook functions reconcile Supabase.


UI trigger locations (where billing starts)

At least three main entry points in the UI trigger billing flows:

  1. Signup → plan selection → checkout

    • /pricing page lets the user select a plan (e.g. ?plan=professional).
    • /signup uses useSignup + useCheckout to:
    • Create user + company.
    • Call create-checkout-session edge function to create a Stripe Checkout Session.
    • User is redirected to Stripe Checkout.
  2. Billing settings page

    • /billing and /settings/billing pages expose subscription details and upgrade/downgrade options.
    • Buttons on these pages typically trigger:
    • create-portal-session edge function → Stripe Billing Portal.
    • or a new call to create-checkout-session for plan changes.
  3. Locked feature upgrade prompts

    • Protected routes with feature flags (e.g. certain tools or analytics) use ProtectedRoute.
    • When a user lacks access, ProtectedRoute renders an "Upgrade Required" card with an UpgradePrompt.
    • The upgrade button eventually routes the user to /billing or triggers a checkout/portal call.

Other parts of the app (e.g. owner dashboard) may surface links to billing, but these three are the primary flow entry points.


Edge functions involved

All billing-related edge functions live under supabase/functions/:

  • stripe-webhook/

    • Receives all Stripe webhooks.
    • Validates signatures using STRIPE_WEBHOOK_SECRET.
    • Ensures idempotency via webhook_events_processed.
    • Updates subscriptions, payment history, and related tables.
  • create-checkout-session/

    • Creates Stripe Checkout Sessions for subscription plans.
    • Ensures a Stripe customer exists for the company.
    • Uses price IDs from env vars (e.g. STRIPE_STARTER_PRICE_ID, STRIPE_PROFESSIONAL_PRICE_ID).
    • Returns a session.url to the frontend.
  • create-portal-session/

    • Creates Stripe Billing Portal sessions for managing payment methods, invoices, and plan changes.
    • Looks up stripe_customer_id from subscriptions (or via Stripe if missing).
    • Returns a portal URL that the frontend opens.
  • sync-stripe-subscriptions/ and sync-payment-history/

    • Periodically reconcile Supabase tables with Stripe data.
    • Useful if any webhook events were missed.
  • get-stripe-subscription/, get-upcoming-invoice/, get-customer-invoices/

    • Read-only helpers for the frontend to display subscription and invoice information.
  • reactivate-subscription/

    • Handles reactivating canceled/paused subscriptions, updating both Stripe and Supabase.

API/Data details

For request/response shapes of each function, see the API/Data → Edge Functions page (once filled out).


Data model (where subscription status lives)

Key Supabase tables (names may vary slightly; see migrations for exact names):

  • subscriptions

    • company_id – which tenant/company the subscription belongs to.
    • stripe_subscription_id – Stripe subscription ID.
    • stripe_customer_id – Stripe customer ID.
    • status – Stripe-status mirror (trialing, active, past_due, canceled, etc.).
    • Plan and billing period fields.
  • webhook_events_processed

    • event_id – Stripe event ID.
    • status, processed_at – track idempotency.
  • Payment history / invoices tables

    • Tables updated by sync-payment-history and invoice-related webhooks.

On the frontend:

  • useSubscription hook reads subscription status (via Supabase).
  • isSubscriptionActive(subscription) determines if the user should have access.
  • getLockoutReason(subscription) provides a human-readable reason for redirects to /billing.

End-to-end subscription flow (diagram)

flowchart LR
  U[User Browser] --> UI[/Billing / Signup UI/]
  UI --> EF1["create-checkout-session (Edge Function)"]
  EF1 --> ST[Stripe Checkout]
  ST -->|User pays / completes| STSUB[Stripe Subscription]
  STSUB -->|webhooks| EF2["stripe-webhook (Edge Function)"]
  EF2 --> SBD[(Supabase: subscriptions + payments)]
  UI -->|check status| SBD

  click UI "../architecture/frontend/" "Frontend billing & signup pages"
  click EF1 "../api/edge-functions/" "create-checkout-session"
  click EF2 "../api/edge-functions/" "stripe-webhook"
  click ST "./billing-stripe/" "Billing flow (this page)"
  click SBD "../architecture/supabase/" "Supabase schema & subscriptions"

Narrative:

  1. User triggers billing (signup, upgrade, or manage billing).
  2. Frontend calls create-checkout-session (or create-portal-session) to get a Stripe URL.
  3. User completes checkout or manages subscription on Stripe.
  4. Stripe fires webhooks; stripe-webhook updates subscriptions and payment tables.
  5. Frontend reads subscription status from Supabase via useSubscription and gates routes with ProtectedRoute.

Where to look next