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:
- A stale
is_procolumn. Your app flips a flag inprofileswhen someone pays, and something else has to flip it back. If that code path never runs, the synced subscription data sayscanceledwhile your app still says Pro. - RLS and route checks that read the wrong thing. The policy checks
profiles.plan, but the webhook (or the new sync) writes somewhere else, and the two never meet. - Edge-case statuses.
past_dueneeds a rule: grace period or lock out?incompletemeans the first payment never succeeded. A subscription withcancel_at_period_end = trueis stillactiveuntil the period ends, so cutting access early is a bug too. - The lag window. Stripe's docs say that during the preview a row can briefly show an earlier state, and some updates can take up to 10 minutes (Real-time sync to Postgres).
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:
- Paying, but locked out: an active or trialing subscription (or a paid one-time purchase) whose user is missing or inactive in your app.
- Cancelled, still inside: your app says active, but the subscription is canceled, unpaid, incomplete or past_due in Stripe.
- Double charges: same customer, same amount, close together. Each comes with a confidence level and can be dismissed.
- Failed webhooks: events your endpoint never acknowledged, disabled endpoints, and endpoints that don't subscribe to subscription or checkout events. These still matter if your app flips access from webhooks.
- One-time buyers and seats: lifetime deals, refunds, Stripe entitlements, and teams using more seats than they pay for.
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 side: PaidCheck reads Stripe directly through a read-only restricted key (
rk_…). Fullsk_live_keys are refused. - App side: you pick one. Paste our prompt into Lovable or Cursor to create a secret-protected Supabase function (or a bearer-protected HTTPS endpoint) that returns
email, plan, active. Or upload a CSV. Or skip it and get the Stripe-only checks. - What it compares: what Stripe says against what your app actually grants. That's the decision the sync can't check for you.
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
- Leak report: $0. One-time check of one app, keys used once and never stored.
- Solo: $19/mo. 1 app, daily checks, alert emails for new leaks, Monday summary.
- Multi-app: $49/mo. Up to 5 apps, daily checks on every app, comp/teammate allowlist.
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.