Skip to content
Get the kit

Supabase

The kit ships two interchangeable backends. This guide covers the Supabase one: the same Hono API on an Edge Function, Postgres instead of Firestore, auth app_metadata instead of custom claims. The extension talks to either backend through the same routes and storage keys, so your components never know which stack is behind them.

Pick the stack in the setup wizard and the other one is pruned from your clone. Everything below assumes a Supabase clone; the step-by-step setup lives in Setup, step 6; the payments commands are below.

The security contract is identical to the Firebase path, enforced by Postgres row security instead of Firestore rules:

  • Entitlements live in the customers table, written only by the Stripe webhook through a compare-and-swap RPC (apply_customer_txn). Clients can read their own row and can never write it.
  • Claims mirror: the webhook mirrors the paid state into auth app_metadata, the Supabase equivalent of custom claims.
  • Usage lands in the usage table via POST /gate/events; anything with stakes (trial-once, credits) is enforced there.
  • Prices resolve by Stripe lookup_key server-side. The extension never sends a price id or an amount.
  • backend/supabase/functions/api/: the Edge Function. index.ts wires routes; store.ts implements the entitlement-store and claims-writer ports on Postgres; stripe-gateway.ts adapts Stripe to the Deno runtime.
  • backend/supabase/functions/api/core/: generated. The pure billing, gate, and error modules are synced from backend/functions/src by scripts/sync-core.mjs; CI fails if the copies drift. Edit the source in backend/functions, then run node scripts/sync-core.mjs.
  • backend/supabase/migrations/: schema, row security, and the two RPCs (apply_customer_txn, increment_usage).

Entitlements and broadcasts stream over Supabase Realtime: the migration adds both tables to the supabase_realtime publication, and the background subscribes. Every resubscribe runs a catch-up fetch, so state written while the service worker slept lands on wake. The UI keeps reading storage.local snapshots either way.

WXT_BACKEND=supabase selects the stack at build time (the wizard sets it in apps/extension/.env). The #backend alias swaps the background wiring, so the Firebase SDK never enters a Supabase bundle. Sign-in uses @extensionstart/core-auth/supabase: chrome.storage-backed sessions, PKCE, Google through chrome.identity.launchWebAuthFlow, and anonymous-first linking with the uid preserved.

The catalog is account-level at the provider, so the same seed scripts apply; secrets and the deploy go through the Supabase CLI. For Stripe, from backend/functions:

Terminal window
STRIPE_SECRET_KEY=sk_test_… pnpm seed:stripe

Then from backend:

Terminal window
supabase secrets set STRIPE_SECRET_KEY=sk_test_…
Terminal window
supabase secrets set BILLING_SUCCESS_URL=https://your-site.example/success
Terminal window
supabase secrets set BILLING_CANCEL_URL=https://your-site.example/cancel
Terminal window
supabase secrets set BILLING_TRIAL_DAYS=7
Terminal window
supabase functions deploy api

Create the webhook in the Stripe dashboard pointing at https://<project-ref>.supabase.co/functions/v1/api/billing/webhook with the five events listed in backend/functions/stripe/create-webhook.mjs, then store its secret and deploy once more:

Terminal window
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_…
Terminal window
supabase functions deploy api

Polar and Paddle work the same way: their access token or API key and webhook secret go through supabase secrets set, and the webhook URL is …/functions/v1/api/billing/webhook.

Dropped the billing module? Set the flag and deploy with no Stripe secrets at all:

Terminal window
supabase secrets set BILLING_DISABLED=true
supabase functions deploy api

The billing routes answer with a clean disabled state and everything else keeps working.