Skip to content
Supabase+Stripe
Stack Integration

Supabase + Stripe Integration Guide

Combining Supabase's Postgres-backed backend with Stripe's payments infrastructure is the standard stack for SaaS MVPs.

Use Cases
  1. Subscription billing tied to Supabase user records
  2. Webhook-driven subscription status updates in Postgres
  3. Multi-tier pricing with feature flags stored in Supabase
  4. Customer portal with billing management
Implementation

The key pattern: Stripe webhooks → Supabase Edge Function → Postgres. User subscription tier lives in Postgres and drives RLS policies. Webhooks run server-side, never expose Stripe secret key to client.

In detail

How the pieces connect

The data flow is one-directional and server-authoritative. Your client (web or mobile) calls Stripe Checkout or the Billing Portal to start a payment, but the source of truth for entitlement is Postgres, not the client. When a payment, subscription, or invoice state changes, Stripe POSTs an event to a webhook handler running as a Supabase Edge Function (Deno). That function verifies the signature, reads event.type (checkout.session.completed, customer.subscription.updated, invoice.payment_failed, and similar), and upserts the relevant row into a subscriptions or customers table.

The link between the two systems is the stripe_customer_id. Create the Stripe Customer once, store its id against the Supabase auth.users id, and every later webhook can resolve back to the right user. The client never decides what tier someone is on; it just reads the row Postgres already holds.

Production gotchas

Webhook verification and idempotency are where most of these break. Verify every payload with stripe.webhooks.constructEventAsync against your endpoint's signing secret — in Deno you need the async variant, because the sync constructEvent relies on a synchronous crypto path that isn't available there. Read the raw request body before parsing; if a framework re-serializes the JSON, the signature check fails.

Stripe retries events and can deliver them out of order, so treat handlers as idempotent: key on event.id or compare the subscription's current_period_end before writing, so a late subscription.updated can't overwrite newer state. Use the Stripe secret key only inside the Edge Function via Deno.env.get — never in client bundles. And note Edge Functions cold-start: the first webhook after idle adds latency, but Stripe's retry window absorbs the occasional timeout.

RLS and entitlement enforcement

The reason to keep tier in Postgres is Row Level Security. Once a subscriptions row carries the user's plan and status, you write RLS policies that gate premium tables on it — for example, USING (auth.uid() = user_id AND EXISTS (SELECT 1 FROM subscriptions s WHERE s.user_id = auth.uid() AND s.status = 'active')). That makes the entitlement check run inside the database on every query, so a tampered client can't read or write rows it hasn't paid for.

Two things to get right: the webhook writes to the subscriptions table with the service-role key, which bypasses RLS, so that table itself needs a policy denying client writes — clients should only ever read their own row. And keep status and cancel_at_period_end in sync with Stripe so access ends when the paid period actually does, not at the cancel click.

When this combo fits — and when it doesn't

This stack fits recurring SaaS billing: subscriptions, tiered plans, metered add-ons, and self-serve plan changes through the Stripe Billing Portal, all anchored to Supabase Auth users. You get Postgres, auth, and serverless webhook handling in one project, which is why it's a common default for an MVP that needs paywalled features fast.

It's a weaker fit when payments aren't tied to logged-in accounts — one-off anonymous purchases, marketplaces splitting funds across many sellers (Stripe Connect adds real complexity here), or platforms that need heavy custom invoicing logic. It's also overkill if you only need a single payment link. If your backend already lives elsewhere (Firebase, a dedicated Node API), run the webhook there instead of adding Supabase purely for billing — the value is in using Postgres + RLS as the entitlement store, not the Edge Function alone.

Live ExampleBookBed SaaS — Case Study
Other integration guidesView all →
Related

Need this built?