FIX-05 · STRIPE SUBSCRIPTIONS · SUPABASE

Cancelled subscriptions still have access (Stripe + Supabase)

Sometimes it's correct behavior wearing a scary mask. Sometimes your database really is frozen on "active" forever. Here's how to tell — and where the missing wire goes.

Updated July 2026 · 5 min read · free self-serve checks inside

A customer cancelled weeks ago. Stripe shows the subscription as cancelled, no new invoices are being paid — and they're still using your product like nothing happened. On the Supabase + Stripe stack this is one of the most common bugs we see, and one of the most commonly mis-diagnosed, because one version of it isn't a bug at all.

First: is this actually correct behavior?

Stripe cancellations come in two flavors. Cancel immediately ends the subscription now. Cancel at period end — the default for most customer-facing cancel buttons — keeps the subscription active until the end of the billing period the customer already paid for, then ends it. A customer who cancels on the 3rd keeps access until the 30th because they paid for the month. That's not a bug; that's the deal.

Check which one you're looking at: Stripe Dashboard → Customers → the customer → their subscription. If it says something like *"Cancels on Aug 3"*, access until Aug 3 is correct — tell the confused customer and move on. If it says Canceled, past tense, with a date already gone by, and your app still lets them in: now it's a bug. Keep reading.

The root cause: a one-way door

Generated subscription code usually gets the happy path right: customer pays, webhook fires, database says active. What's missing is the return path. active gets written once at checkout, and no code ever writes anything else. Cancellations, failed renewals, refunds — Stripe knows about all of them and sends an event for each. Your app just never subscribed to the news.

Check your webhook's event list (3 minutes)

In Stripe, open the Developers area (Workbench) → Webhooks → your endpoint, and read the "Listening to" section. For subscriptions to stay in sync, the endpoint needs, at minimum:

  • customer.subscription.updated — plan changes, renewals, a cancellation being scheduled
  • customer.subscription.deleted — the subscription actually ending; this is the event that should cut access
  • invoice.payment_failed — a renewal charge failing, the start of the past-due path

If the endpoint only listens to checkout.session.completed, you've found it: Stripe has been announcing every cancellation, and your app asked not to be told. Helpfully, when a period-end cancellation finally ends, Stripe sends customer.subscription.deleted at that moment — so one handler covers both cancel flavors.

Check whether the events arrived — and what happened next

Open the Events tab and search for customer.subscription.deleted. Find one belonging to a cancelled customer. Was it delivered to your endpoint at all? With what response? A 200 while the customer stays active in Supabase means the handler received the event and did nothing useful with it — usually there's no code path for that event type, so it falls through to a generic "return 200".

Cross-check in Supabase → Table Editor: the cancelled customer's row. If the status column still reads active and the row's updated_at is checkout day, nothing has written to it since. That's your one-way door, visible in the data.

Stopping the leak today

The honest immediate fix is manual: compare Stripe's cancelled-subscriptions list against your table, and flip the stale rows by hand in Table Editor. Not elegant — but it stops free usage today, and there are usually fewer such customers than you fear.

The durable fix means adding the missing event types to the endpoint and writing handler code for each: mapping Stripe's subscription states onto your database's idea of access, including the awkward ones — past-due grace periods, resubscribes, refunds. That mapping is where generated code reliably runs out of depth, and it's the bulk of the work when we fix this one. If you'd rather not learn Stripe's subscription lifecycle this week, the diagnosis below is free.

STILL BROKEN? THAT'S THE HARD VERSION

Diagnosed free. Fixed for a flat $250. Proven live.

01 · FREE HEALTH CHECK

Describe the symptom in two minutes. You get a plain-English root cause within hours — yours to keep even if you fix it yourself. No call, no strings.

02 · THE $250 FLAT FIX

One broken flow, fixed within 48 hours of access — proven with a recorded live test transaction you watch, and covered by a 30-day warranty. Miss the deadline, pay nothing.

QUICK ANSWERS

?Is it a bug if a cancelled customer keeps access until the end of the month?

Usually not. "Cancel at period end" — the default for most cancel buttons — keeps access through the period the customer already paid for. It's only a bug if the subscription shows as fully Canceled in Stripe, the period is over, and your app still grants access.

?Which Stripe events do subscriptions need?

At minimum: checkout.session.completed to grant access, customer.subscription.updated for changes, customer.subscription.deleted to remove access when the subscription actually ends, and invoice.payment_failed to catch failed renewals.

?Why does my database still say active when Stripe says cancelled?

Because nothing ever wrote to it after checkout. Either the webhook endpoint isn't subscribed to the subscription lifecycle events, or the handler receives them and has no code path for them. Stripe's Events log tells you which of the two it is.