Supabase // Stripe

Stripe Sync Engine

Erfi Anugrah

The Backend AI Builds On

Supabase is one Postgres platform - the operational and AI backend that replaces a stack of vendors. It’s now the backend AI app-builders plug into.

  • >60% of new databases are created by an AI tool · 600% YoY launch growth
  • The backend integration behind Lovable · Bolt · Figma Make

The Platform: Everything Is Postgres

One platform, one bill - and everything lands in Postgres tables.

SB sb Supabase ab App backend sb--ab ai AI sb--ai an Analytics & data movement sb--an sc Scale sb--sc d1 Database · Postgres + RLS ab--d1 d2 Auth · OAuth / SAML / MFA ab--d2 d3 Data APIs · PostgREST + GraphQL ab--d3 d4 Realtime · Broadcast / Presence / CDC ab--d4 d5 Storage · S3-compatible + CDN ab--d5 d6 Edge Functions · Deno runtime ab--d6 d7 Cron & Queues · pg_cron / pgmq ab--d7 v1 pgvector · semantic + hybrid search ai--v1 n1 Analytics Buckets · Iceberg an--n1 n2 Pipelines · CDC → BigQuery an--n2 n3 Wrappers · FDW → Snowflake / ClickHouse an--n3 st today · GA sc--st sr roadmap sc--sr t1 Supavisor · connection pooler st--t1 t2 Read Replicas · read scaling st--t2 r1 Multigres · Vitess for Postgres sr--r1 r2 OrioleDB · storage engine sr--r2

Three Differentiators

Open Source

107K+ stars · self-hostable

Postgres-Native

Standard SQL + the full extension ecosystem

No Lock-in

pg_dump anytime - your data, your keys

What Customers Are Building

Pattern Example Why Supabase
Startup greenfield Chatbase - $10M+ ARR, bootstrapped Zero to $10M on one stack
DB + Auth consolidation Good Tape - 60% cost cut Auth0 + Fly → Supabase
Multi-tenant SaaS Kayhan Space - 8x dev speed RDS + Auth0 → DB + Auth, RLS per tier

Sources: Chatbase · Good Tape · Kayhan Space - Supabase customer stories.

Billing Data and Product Data Don’t Talk

Your billing data lives in Stripe. Your product data lives in Postgres.

  • “Which customers are on which plan?”
  • “What do paying users actually do in the product?”
  • “Which accounts are about to churn?”

Answering any of them means joining the two.

FDW, Hand-Rolled, or Sync Engine

Approach Latency Joins Rate limits
Stripe FDW 0.4 - 1.3 s painful yes
Hand-rolled pipeline good good your problem
Stripe Sync Engine under 1 ms native SQL handled

Copy the data into real Postgres tables. Query it like everything else.

You buy the maintenance, not the install. One click to start · built jointly by Stripe + Supabase · open source (Apache 2.0)

How It Works

Sync stripe Stripe q pgmq queues stripe->q backfill ef Edge Functions stripe->ef webhooks q->ef db stripe schema ef->db yt your tables users · usage db->yt SQL joins

  • Install is the easy part: paste a restricted key, click Install - webhooks + backfill configure themselves.
  • You buy the long tail: 29 tables in your stripe schema - 24 object types - kept in sync, with idempotent webhooks, rate limits and Stripe’s field-moves handled for you.

What You’re Looking At

  • A multiplayer game on Supabase - hosted in this region
  • A Stripe test-mode account - seeded with two plans and paying customers mapped to those users.

Join on Internal IDs, Not Email

The one thing to copy into your own app, whatever else you take away:

join players p
  on p.id::text = c.metadata->>'player_id'
  • Your internal user ID goes into the Stripe customer’s metadata at checkout
  • Emails change or never exist - only 2 of my users have one on file

After that, every billing question is a SQL join away.

Who Signed Up but Never Paid?

Users, account older than a week, no subscription on file:

14 users - pulled live, one query.

handle signed up
bruno222 2026-06-24
Slack-Lever-4796 2026-06-24
Lean-Saw-7283 2026-06-24
11 more

That’s a re-engagement list. Against the Stripe API you’d paginate customers and correlate by hand.

What’s the MRR?

plan subscribers mrr
Premium Monthly 9 $45.00
Premium Yearly 4 $13.33

Live result, milliseconds. Yearly normalized to monthly - a naive sum overstates annual plans 12x, and getting that wrong on a dashboard is how you misreport to your own team.

Who’s About to Churn?

Paying users with no product activity in 7 days, by renewal date:

handle renews last played
Bluff-Saw-7849 2026-08-29 2026-07-29
Even-Spring-2034 2026-08-29 2026-07-29
Wary-Wedge-2309 2026-08-29 2026-07-26
5 more

8 of 13 paying users. The other five played within the week - the metric correctly leaves them off. Dormant by the 7-day window is what the metric shows, not a guarantee they will churn.

Or we can just ask

Once billing is tables, your agent can answer in plain English - via Supabase’s MCP server from your agent/tool of choice

you ask it runs you get
“who never converted?” NOT EXISTS join 14 users
“MRR by plan?” subscription_items aggregate $58.33
“who’s churning?” 7-day activity join 8 of 13

One URL: project-scoped, read-only, database tools only. OAuth in the browser - no keys to manage.

Demo

“which paying users should I win back this week, and when do they renew?”

The agent inspects the schema, writes the SQL, runs it against the branch, and answers - names and renewal dates

Takeaways

  1. Billing is relational data. Put it where your other relational data lives.
  2. Buy the maintenance, not the install. The webhook is a day’s work; the long tail is what’s managed.
  3. Join on your internal ID in Stripe metadata - never email.
  4. Conversion, MRR, churn become SQL you already know - or a question you hand to an agent.

Appendix: the SQL

Never-converted (slide 11):

select p.handle, p.created_at as signed_up
from players p
where p.created_at < now() - interval '7 days'
  and not exists (
    select 1 from stripe.customers c
    join stripe.subscriptions s on s.customer = c.id
    where c.metadata->>'player_id' = p.id::text )
order by p.created_at;

NOT EXISTS, not LEFT JOIN … IS NULL - players can have multiple Stripe customer rows; only one needs a subscription.

Appendix: MRR + churn SQL

-- MRR by plan: price lives on subscription_items, not the subscription
-- (yearly normalized: divide by # of months)
select prod.name, count(*),
  sum(p.unit_amount * case when p.recurring->>'interval' = 'year'
      then 1.0 / 12 else 1 end) / 100.0 as mrr
from stripe.subscriptions s
join stripe.subscription_items si on si.subscription = s.id
join stripe.prices p   on p.id = si.price #>> '{}'
join stripe.products prod on prod.id = p.product
where s.status = 'active' group by prod.name;
-- at-risk: read the period from subscription_items, not subscriptions
-- current_period_end is NULL on subscriptions; Stripe API 2025-03-31.basil moved it to the item.
-- It survives in subscription_items._raw_data as a bigint timestamp.
having coalesce(max(r.started_at), '-infinity'::timestamptz)
       < now() - interval '7 days'

Appendix: when Stripe’s API changes

When the API moves, you change a query in your own database - not a support ticket, not a vendor’s roadmap.

It already happened on this account. Stripe’s 2025-03-31.basil release moved the renewal date from subscriptions to subscription_items; when the account’s API version caught up, a routine sync rewrote the rows and the old column read NULL. No error, no alert.

The full payload survives: every synced table keeps a _raw_data jsonb column, and the typed columns are projections of it. The fix was one line of SQL reading the new location - subscription_items._raw_data ->> 'current_period_end'. Everything else - customers, invoices, charges, the joins you just watched - never stopped syncing.