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>
React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- @vitejs/plugin-react uses Babel for Fast Refresh
- @vitejs/plugin-react-swc uses SWC for Fast Refresh
Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
You can also install eslint-plugin-react-x and eslint-plugin-react-dom for React-specific lint rules:
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})