Stripe Sync puts your data in Supabase. PaidCheck checks your app actually obeys it.

Your Stripe subscriptions can now land in Postgres automatically. Your app still decides who gets in, and nothing checks that decision. On a paid plan, PaidCheck compares Stripe with who actually has access in your app daily, and sends an alert email when a check finds a new issue.

Run the free leak report →Read-only restricted key · keys used once, never stored

What Stripe Sync gives you

On October 15, 2026, Stripe launched real-time sync to Postgres through Stripe Data Pipeline, with Supabase as one of the first destinations you can connect from the Stripe Dashboard (Stripe blog, Supabase blog). You connect a Supabase project, select the tables you want, and Stripe backfills your history and keeps it in sync. Charges, customers, subscriptions, invoices and more land as tables in a Stripe-managed schema in your own database; the schema's name depends on your setup (Stripe docs). According to Supabase's announcement, Stripe connects through a dedicated role scoped to that schema, with short-lived credentials instead of a stored database password. Stripe's docs say most changes appear within a few seconds. It's in public preview.

That's a real upgrade: no webhook handlers to write just to keep a copy of your billing data.

What it doesn't do

The sync moves data. It doesn't decide anything. Access in a vibe-coded app is usually decided somewhere else:

The result is the same leak in both directions: paying users locked out and cancelled users still inside. Nothing errors, so nobody notices.

What PaidCheck checks daily

PaidCheck compares Stripe with who actually has access in your app, ranks mismatches by severity and revenue at risk, and gives you fix instructions for each one:

Paid plans run checks daily, send an alert email when a check finds a new issue, and send a Monday summary.

How it fits next to Stripe Sync

PaidCheck works alongside Stripe Sync. It doesn't replace it and doesn't depend on it.

Stripe Sync keeps your copy of Stripe current. PaidCheck checks that your app acts on it. See the setup guide for the exact permissions and prompts.

DIY check: one query you can run today

Example only. Adapt it to your schema. It assumes the synced schema is named stripe (Stripe's docs give that as the default for connection-string setups; with Supabase's OAuth setup it's typically stripe_sdp-prefixed, and you may have picked your own name) and a public.profiles table with email, stripe_customer_id and a boolean is_pro. Table and column names can differ depending on your setup; check Stripe's real-time sync schema. If your app uses a plan column, a separate subscriptions table, teams or one-time purchases, change it to match. Run it read-only.

-- EXAMPLE: compare synced Stripe subscriptions with your app's access flag.
-- Rename stripe.subscriptions / public.profiles / is_pro to match your schema.
with best_sub as (
  -- one row per customer: prefer an active/trialing sub, else the newest
  select distinct on (customer)
         customer, id as subscription_id, status, cancel_at_period_end, _updated_at
  from stripe.subscriptions
  where livemode
  order by customer,
           (status in ('active', 'trialing')) desc,
           created desc
)
select
  p.email,
  p.is_pro,
  s.status,
  s.cancel_at_period_end,
  s._updated_at,
  case
    when s.status in ('active', 'trialing') and not coalesce(p.is_pro, false)
      then 'paying_but_locked_out'
    when coalesce(p.is_pro, false) and coalesce(s.status, 'none') not in ('active', 'trialing')
      then 'access_without_paying'   -- decide your rule for past_due
  end as issue
from public.profiles p
full outer join best_sub s on s.customer = p.stripe_customer_id
where (s.status in ('active', 'trialing') and not coalesce(p.is_pro, false))
   or (coalesce(p.is_pro, false) and coalesce(s.status, 'none') not in ('active', 'trialing'));

What this sketch misses: users with no stripe_customer_id (you'd need to match by email through the synced customers table), one-time purchases, seats, double charges, webhook failures, and rows still inside the sync's lag window. Rows with no profile at all show up with an empty email. That's the work PaidCheck does for you, daily.

FAQ

Doesn't Stripe's Supabase sync fix billing bugs on its own?

It fixes getting a copy of your Stripe data into your database. It doesn't change the code that grants or removes access in your app: your is_pro flag, RLS policies, middleware, or how you handle past_due and cancellations. That code is where paid and access drift apart.

Does PaidCheck read the synced Stripe schema in my database?

No. PaidCheck reads Stripe directly through a read-only restricted key, and reads your app's access through a secret-protected Supabase function, an HTTPS endpoint, a CSV, or not at all (Stripe-only checks). It works the same whether or not you use Stripe's sync.

Do I need Stripe Data Pipeline to use PaidCheck?

No. PaidCheck doesn't depend on how your app keeps billing in sync: webhooks, the older Stripe Sync Engine, the new Data Pipeline sync, or a success-page redirect.

How does PaidCheck treat cancel_at_period_end and past_due?

A subscription set to cancel at period end is still active, so a user with access isn't flagged until Stripe marks it canceled. Once it's canceled and the user still has access, that's flagged. If your app says active while Stripe says canceled, unpaid or incomplete, that's flagged as high severity, and past_due as medium.

Is it safe to connect?

PaidCheck asks for a read-only Stripe restricted key, refuses full sk_live_ keys and Supabase service-role keys, and never writes to Stripe. The free report uses your keys for one request in memory and stores nothing. Keys for monitored apps are encrypted at rest.

Pricing

Sources: Stripe blog (Oct 15, 2026) · Stripe docs: real-time sync to Postgres · Stripe docs: configure Postgres · Supabase blog. "Stripe Sync" is our shorthand for Stripe Data Pipeline's real-time sync to Postgres, not Supabase's older Stripe Sync Engine.