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:
- Stripe — who paid, which plan, subscription status
- 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:
- 84% use or plan to use AI tools (Stack Overflow 2025 survey, 49k+ responses)
- 90% of software professionals use AI daily (Google DORA 2025)
Trust did not keep up:
- 29% trust AI accuracy, down 11 points from 2024 (Stack Overflow blog, Feb 2026)
- 46% distrust AI output accuracy (SO 2025 press release)
- 66% say their top frustration is output that is "almost right, but not quite" (InfoWorld on the survey)
AI is good at checkout → stub webhook → set has_paid_access = true. It is weaker on the paths that actually cause drift:
-
invoice.payment_failedwithout revoking access - Out-of-order
customer.subscription.updatedevents - 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
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
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)
- Stripe owns subscription status. Your DB is a cache.
- Store processed
evt_...IDs with a unique constraint. - Refetch the subscription from Stripe on critical events. Do not assume event order.
- Run a nightly reconciliation: active Stripe subs vs DB rows, alert on mismatch.
- 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
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?
Top comments (0)