Problem: if stripe listen is not running (dev) or the webhook secret is
misconfigured, a successful checkout leaves the user stuck on the free
plan in the DB even though Stripe knows they're subscribed.
Solution: 3 recovery mechanisms.
1. Backend: POST /stripe/sync (auth required)
Fetches the current user's subscriptions from Stripe by customer ID,
picks the most recent active/trialing/past_due one, and applies it to
the User row via the same applySubscriptionToUser helper used by the
webhook. If no active sub exists, downgrades to free. Returns the
current plan state.
2. Frontend: CheckoutSuccess now calls /stripe/sync first (instant,
reliable) before falling back to polling /stripe/subscription. This
fixes the 'just paid but still free' bug even with no webhook setup.
3. Frontend: 'Rafraîchir' button on the Profile free-plan upgrade banner
(ghost style with RefreshCw spinning icon). Tooltip hints at its
purpose. Users who paid but see the free state can click it to
self-heal in one click.
4. Backend script: scripts/sync-subscription.ts
- npm run stripe:sync -- user@example.com (sync one user by email)
- npm run stripe:sync -- --all (sync every user with a
stripeId, useful after
a prod webhook outage)
Colored output with ✓ / ✗ / ↷ status per user.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>