Diagnostic guide

stripe webhook not updating database

Stripe recorded the payment. Your database did not. The gap between those two facts is almost always one of five things, and four of them look identical from the Stripe dashboard: the endpoint shows a 200 and you move on. The difference is whether the write happened, not whether the request arrived.

most likely causes, in order

01The handler returns 200 before the write finishes+

how it looks

Stripe shows a successful delivery. The row is unchanged. Nothing is logged, because nothing threw where you were looking.

how to check

Find the response in your handler and check what runs after it. If the database call is not awaited before the response, or sits in a fire-and-forget promise, this is it.

the fix

Await the write, then respond. Return 500 on failure so Stripe retries. A 200 tells Stripe to stop trying, and it will.

02The endpoint URL changed on a redeploy+

how it looks

Everyone who signed up before a certain date is fine. Everyone after is broken. The date is your last deploy.

how to check

Stripe Dashboard → Developers → Webhooks. Compare the registered URL against the route that exists in production right now. Preview URLs and renamed routes are the usual culprits.

the fix

Point the endpoint at the production URL, then replay the failed events from the dashboard. They are not gone.

03Signature verification fails silently+

how it looks

Deliveries show 400. Easy to miss if you only look at whether events were sent.

how to check

Your signing secret must match the endpoint that received the event. Test and live secrets differ, and so does each endpoint's.

the fix

Verify against the raw request body. Any framework that parses JSON before you verify will break the signature.

04RLS blocks the write from inside the handler+

how it looks

The handler runs, the query succeeds, zero rows are affected. Common on Supabase.

how to check

Check which key the handler uses. The anon key is subject to row-level security and has no authenticated user in a webhook context.

the fix

Use the service-role key in the webhook only, server-side. Never expose it to the browser.

05The user reference was never written back+

how it looks

A row exists with a Stripe customer id you cannot match to anyone, or no row at all.

how to check

Look for client_reference_id or metadata on the Checkout Session. If it is empty, the link between payer and account was never made.

the fix

Set client_reference_id when you create the session, and read it in checkout.session.completed.

check it yourself

Find rows that were paid for but never granted access. Run this in your SQL editor, adjusting the table and column names to match your schema.

-- everyone with a Stripe customer id but no access
select id, email, stripe_customer_id, subscribed, created_at
from public.subscribers
where stripe_customer_id is not null
  and subscribed = false
order by created_at desc;

-- and the opposite: access with no payment record
select id, email, subscribed, subscription_tier
from public.subscribers
where subscribed = true
  and stripe_customer_id is null;

what this does not tell you

This tells you who is broken in your database. It cannot tell you who is broken in Stripe: a customer who cancelled two months ago and still has access has a perfectly normal-looking row. That direction only shows up if you compare the two records against each other.

Read-only, both sides. Three findings free, no account.

related