Skip to content

Tool-pair integration pattern

Stripe + Supabase integration

Stripe + Supabase is the dominant indie SaaS payment + database integration. Stripe owns checkout and subscription state; Supabase owns user identity and product state. The integration glue is a webhook handler that reflects Stripe events into Supabase tables.

Verified · editorial policy

Stripe owns (teardown →)

Customer payment methods, subscription lifecycle, invoice generation, refunds, taxes (if MoR or Stripe Tax enabled), pricing tiers.

Supabase owns

User authentication, user profile, application data (what the user paid FOR), role-based access control, audit log.

Integration shape

Webhook-driven. Stripe fires events to your endpoint; your endpoint authenticates the signature, then updates Supabase tables (customers, subscriptions, paid_features) to reflect the Stripe state. Application reads Supabase only — never queries Stripe in the hot path.

Implementation steps

  1. Step 1

    Set up a Stripe Customer record at signup (or first purchase)

    When a user signs up in Supabase, create a Stripe Customer with metadata pointing back to the Supabase user_id. Store the Stripe customer_id on the Supabase user row.

    Gotcha: Creating a Stripe Customer on every login (instead of first signup) produces duplicate customers. Use upsert keyed on user_id metadata.

  2. Step 2

    Build the webhook endpoint

    Single Next.js route handler that receives all Stripe events. Verify the signature with stripe-signature header + your webhook secret. Reject anything that fails.

    Gotcha: Webhook signature verification requires the raw body, not the parsed JSON. Use the framework-specific raw-body access; do not let body parsing destroy the signature.

  3. Step 3

    Handle the 4-5 events you actually need

    Most indie SaaS only needs: checkout.session.completed (first payment), customer.subscription.updated (status changes), customer.subscription.deleted (cancellation), invoice.payment_succeeded (renewals), invoice.payment_failed (dunning).

    Gotcha: Subscribing to all events floods your endpoint with noise. Pick the events you handle; ignore the rest in Stripe Dashboard.

  4. Step 4

    Make the webhook handler idempotent

    Stripe retries failed webhooks for 3 days. Your handler must produce the same end-state regardless of how many times the event arrives. Key idempotency on event.id and store processed event IDs.

    Gotcha: Idempotency-by-side-effect fails. A handler that 'just runs' a second time produces duplicate emails, duplicate access grants, duplicate row inserts. Explicit dedup is required.

  5. Step 5

    Reflect subscription state into Supabase

    Keep a subscriptions table in Supabase mirroring the relevant Stripe state (status, plan, current_period_end). Application reads this; never queries Stripe at request time.

    Gotcha: Querying Stripe at every page load is the single most common mistake. Stripe rate limits and adds latency; Supabase reads are 10-100x faster.

  6. Step 6

    Use Row-Level Security (RLS) for access control

    Supabase RLS policies check the user's subscription row to gate access to paid features. The policy reads from the subscriptions table, not from Stripe.

    Gotcha: RLS policies that query Stripe-side data cannot work. RLS runs at query time; only Supabase-side data is available.

  7. Step 7

    Test the full flow with Stripe CLI before deploying webhooks

    stripe listen --forward-to localhost:3000/api/webhook lets you trigger events locally. Test customer.created, subscription.updated, subscription.deleted, payment.failed before going to production.

    Gotcha: Production webhook secret differs from CLI's local secret. Use environment-specific secrets and verify both.

Common gotchas across the whole integration

  • Storing Stripe data in Supabase as the source of truth, then drifting from Stripe's actual state. Stripe is always the source of truth; Supabase mirrors.
  • Missing webhook events between retries — your endpoint must be idempotent and tolerate replays.
  • Not handling the gap between subscription.created and the first invoice.paid event. The customer can be 'subscribed' but unpaid for 60 seconds; design for it.
  • Hardcoding Stripe price IDs in application code. Store them in a config table; new prices should not require a deploy.

When NOT to build this integration

If you only need one-time payments (no subscriptions), Stripe Payment Links + a single webhook handler is simpler than the full Stripe + Supabase pattern. The full pattern is for SaaS with recurring billing.

Frequently asked

Should I use Stripe Checkout or Stripe Elements?
Stripe Checkout for indie SaaS — hosted checkout that Stripe maintains. Elements is for custom checkouts where you need pixel-perfect control. Most indie SaaS should use Checkout until the volume justifies the Elements engineering cost.
Where should webhook secrets live?
Environment variables, never in code. Use Vercel env vars (or platform equivalent) with one secret per environment. Production webhook secret is the one Stripe Dashboard shows for your production endpoint.

See the stack this integration belongs to

Integration patterns assume the right stack underneath. Use the stack recommendations to pick the right tools first, then the integration to wire them together.

🚀 Explore Our Network

Full disclosure: UnlockSaaS is one of ten small products built and run by one independent operator. These are the other nine.

60 days
To First Paying Customer
7 steps
Proven Playbook
100%
Money-Back Guarantee
$49
Founding Price /mo

You shipped. Nobody paid. The playbook breaks the pattern or the code refunds you automatically.

Get Free Diagnosis

Refund runs from your dashboard, not a support ticket — the server re-checks eligibility and issues it through Stripe automatically.