Stripe wiring

The three plans

Free      — 3 members, no team features
Team      — $29/mo, 10 members, 90d audit retention
Business  — $99/mo, unlimited members, forever audit retention

Plans are test-mode only in the demo. No real charges, ever.

Environment

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_TEAM=price_...
STRIPE_PRICE_BUSINESS=price_...

Without these, the billing UI runs in "not configured" mode — plans are visible, Checkout/Portal buttons return an error.

The webhook (load-bearing)

supabase/functions/stripe-webhook verifies the signature with STRIPE_WEBHOOK_SECRET, then runs insert-first idempotency:

insert into processed_stripe_events (event_id, event_type)
values ($1, $2)
on conflict (event_id) do nothing
returning event_id

If RETURNING is empty, the function returns 200 immediately, before any side effect. This is the right pattern under Stripe's at-least-once delivery.

Check-then-insert is racy and is the textbook bug: two concurrent webhook deliveries both see "not processed," both run the side effect, then one INSERT fails on the unique key — but the side effect already ran twice. The pgtap concurrency test in tests/10_concurrency.sql asserts the correct shape.

State transitions

  • checkout.session.completed — upserts subscription row with metadata-resolved workspace_id
  • customer.subscription.updated — UPDATE in place
  • customer.subscription.deleted — UPDATE status = canceled
  • invoice.payment_failed— audit/telemetry only; relies on Stripe's subsequent updated event for state
  • customer.subscription.trial_will_end — no state change; trial banner reads from trial_ends_at live

Retention

processed_stripe_eventsretains 90 days. Stripe's documented redelivery window is ~3 days; 90 is generous. The pg_cron job in 0110_pg_cron_retention.sql handles the daily sweep.

Hard-delete cascade

When a workspace is hard-deleted (24h after soft-delete), thecancel-stripe-subscription Edge Function cancels the Stripe sub with prorate=false. Failure blocks the hard-delete — orphan Stripe customers continuing to charge is the failure mode this guards against.