DEV Community

Cover image for Stripe says active. Postgres says false. Nobody alerts you.
Matthew
Matthew

Posted on • Originally published at prodverdict.com

Stripe says active. Postgres says false. Nobody alerts you.

Your Stripe dashboard looks fine. MRR is up. Tests pass. CI is green.

Then a customer emails you: "I paid, but I'm still on the free plan."

That is usually not a Stripe bug. Stripe charged them. Your app never updated the row that gates access.

Two systems, one gap

Most subscription SaaS runs on two truths:

  1. Stripe — who paid, which plan, subscription status
  2. Your database — has_paid_access, plan, stripe_customer_id

The webhook handler is supposed to keep them aligned. When it drifts, neither side raises an alarm. Stripe thinks everything delivered. Your app serves the wrong access level. You only find out when someone complains.

Stripe's docs say this plainly:

  • Events are not guaranteed to arrive in order (docs.stripe.com/webhooks)
  • Failed deliveries retry for up to three days in live mode
  • Your endpoint must return 2xx before slow work, or you hit timeouts and retries

This shows up in production, not just in tutorials. On the Ghost forum, a founder paid through Stripe successfully, then landed back on the site as a free user. Ghost staff explained: if the webhook never lands, Ghost never learns about the subscription. Another user confirmed multiple paid subscribers stuck as free until they watched the webhook log constantly.

AI makes the happy path faster and the failure path easier to skip

Developers are using AI tools everywhere:

Trust did not keep up:

AI is good at checkout → stub webhook → set has_paid_access = true. It is weaker on the paths that actually cause drift:

  • invoice.payment_failed without revoking access
  • Out-of-order customer.subscription.updated events
  • Duplicate evt_... IDs without idempotency
  • Writing to the wrong user row for a stripe_customer_id

An Indie Hackers thread from April 2026 describes three Cursor-built SaaS products with the same handler: return 200, log the event, never touch the database. Cancelled users keep Pro. MRR looks fine. You only see the leak when you manually compare Stripe to Postgres.

This dev.to post from sravan27 lists the same bugs in the wild: trusting the success redirect, skipping idempotency, letting Pro access drift from the payment source of truth.

Reproduce a revenue leak in 60 seconds (no API keys)

git clone --depth=1 https://github.com/prodv-dev/prodverdict-sdk.git
cd prodverdict-sdk

npx prodverdict check access \
  --config examples/nextjs-stripe/prodverdict.yml \
  --fixtures \
  --fixtures-dir examples/nextjs-stripe/scenarios/fail-revenue-leak
Enter fullscreen mode Exit fullscreen mode

You should see something like:

[HIGH] user:usr_alice — Active subscription but has_paid_access is false
fix: Set has_paid_access=true in your webhook handler
VERDICT: FAIL
Enter fullscreen mode Exit fullscreen mode

That is a production contract: compare live billing state to live database state. Not a lint rule. Not an LLM guess. PASS / WARN / FAIL.

What fixes this (with or without a tool)

  1. Stripe owns subscription status. Your DB is a cache.
  2. Store processed evt_... IDs with a unique constraint.
  3. Refetch the subscription from Stripe on critical events. Do not assume event order.
  4. Run a nightly reconciliation: active Stripe subs vs DB rows, alert on mismatch.
  5. Block merges when drift exists, not just when mocked unit tests pass.

jest.mock('stripe') will never catch "Stripe says active, Postgres says false."

What I'm building

I run ProdVerdict, a deterministic contract checker for AI-assisted SaaS. The access contract compares Stripe or Paddle to your Postgres users table. Config, migration, boundary, webhook, and restore contracts ship in v0.9.

  • CLI + GitHub Action
  • MCP for Cursor agents (billing secrets stay local)
  • Free for public repos; fixtures above work without credentials
npx prodverdict init --stack nextjs-stripe
Enter fullscreen mode Exit fullscreen mode

I'm looking for three design partners on real Stripe + Postgres repos to run nightly access checks and publish anonymized drift findings. Comment if you want a free audit with a read-only Stripe key.

Have you ever found someone paying in Stripe but locked out in your app, or cancelled but still on Pro? How did you catch it?

prodverdict.com · GitHub · Discord

Top comments (0)