Diagnostic guide

stripe plan upgrade not updating in app

Checkout writes the plan once. An upgrade or downgrade later is a different event entirely, on a subscription that already exists, and it is easy to build the first path and never wire up the second, because the first one is the only one you tested.

most likely causes, in order

01The plan is written on checkout, but never again+

how it looks

New customers get the right plan. An existing customer who upgrades or downgrades later keeps showing their original one, sometimes for months.

how to check

Search for wherever your plan column is written. If the only place is inside the checkout.session.completed handler, an upgrade (which does not create a new checkout) never touches it.

the fix

Also write the plan in the customer.subscription.updated handler, reading it from the subscription's current price or product, not from anything cached at signup.

02The upgrade handler reads the wrong field on the event+

how it looks

The plan sometimes updates and sometimes does not, with no obvious pattern.

how to check

A subscription.updated event includes previous_attributes. If the handler only reacts when one specific field it expects has changed, a coupon, quantity change, or multi-item update can fire the event without tripping that check.

the fix

Read the new plan from the subscription's current items directly on every subscription.updated event, rather than trying to detect which field changed.

03The app's plan gate and the value the webhook writes use different names+

how it looks

The database column visibly holds the correct new plan, and the UI still shows the old one.

how to check

Find the exact condition the app's plan gate reads. A mismatch between a Stripe price nickname ("Pro Monthly") and an app-side plan key ("pro") means the write succeeded and the read still fails.

the fix

Keep one mapping from Stripe price id to your internal plan name, used by both the webhook and the UI, instead of comparing display strings in two places.

04Upgrades made through the Stripe customer portal are invisible to the handler+

how it looks

Upgrades through your own in-app button work. Upgrades a customer makes through the Stripe-hosted billing portal do not.

how to check

The portal changes the subscription directly; from your webhook's point of view it looks exactly like any other customer.subscription.updated. A handler that assumes plan changes only happen through your own API call misses these.

the fix

There is only one source of truth for a plan change: the subscription.updated event, regardless of which UI caused it.

what this does not tell you

A stale plan name does not show up from your own table: it is a perfectly valid-looking row, matching exactly what checkout originally wrote. The only way to find the ones that drifted is to compare the current plan in Stripe against what is stored here, customer by customer.

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

related