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 same authority map
Section titled “The same authority map”The security contract is identical to the Firebase path, enforced by Postgres row security instead of Firestore rules:
- Entitlements live in the
customerstable, 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
usagetable viaPOST /gate/events; anything with stakes (trial-once, credits) is enforced there. - Prices resolve by Stripe
lookup_keyserver-side. The extension never sends a price id or an amount.
Layout
Section titled “Layout”backend/supabase/functions/api/: the Edge Function.index.tswires routes;store.tsimplements the entitlement-store and claims-writer ports on Postgres;stripe-gateway.tsadapts Stripe to the Deno runtime.backend/supabase/functions/api/core/: generated. The pure billing, gate, and error modules are synced frombackend/functions/srcbyscripts/sync-core.mjs; CI fails if the copies drift. Edit the source inbackend/functions, then runnode scripts/sync-core.mjs.backend/supabase/migrations/: schema, row security, and the two RPCs (apply_customer_txn,increment_usage).
Live state without polling
Section titled “Live state without polling”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.
The extension build
Section titled “The extension build”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.
Payments
Section titled “Payments”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:
STRIPE_SECRET_KEY=sk_test_… pnpm seed:stripeThen from backend:
supabase secrets set STRIPE_SECRET_KEY=sk_test_…supabase secrets set BILLING_SUCCESS_URL=https://your-site.example/successsupabase secrets set BILLING_CANCEL_URL=https://your-site.example/cancelsupabase secrets set BILLING_TRIAL_DAYS=7supabase functions deploy apiCreate 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:
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_…supabase functions deploy apiPolar 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.
Billing off, or on later
Section titled “Billing off, or on later”Dropped the billing module? Set the flag and deploy with no Stripe secrets at all:
supabase secrets set BILLING_DISABLED=truesupabase functions deploy apiThe billing routes answer with a clean disabled state and everything else keeps working.