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>
New script backend/scripts/setup-stripe.ts that:
- Reads STRIPE_SECRET_KEY from .env
- Detects test vs live mode and warns + 5s delay for live
- For each plan (Essentiel 3EUR/mo, Premium 5EUR/mo):
- Looks up existing price by lookup_key (freedge_essential_monthly,
freedge_premium_monthly) — idempotent, safe to re-run
- If missing, creates the product then the recurring price with the
lookup_key and nickname for clarity
- Prints the resulting price IDs with their env var names
- With --write-env flag, automatically upserts the values into
backend/.env preserving other lines
- Points to Customer Portal settings and stripe listen command as
next steps
npm scripts added:
- npm run stripe:setup # dry run, just print IDs
- npm run stripe:setup:write # update .env automatically
- npm run stripe:listen # shortcut for stripe CLI webhook forward
Updated README to show the script as the recommended path for step 1,
keeping the manual dashboard instructions as a fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Image generation:
- Automatic model fallback: try gpt-image-1 first, fall back to
dall-e-3 if it fails (e.g. org not verified on OpenAI)
- Local filesystem fallback: if MinIO upload fails, write the image
to backend/uploads/recipes/ and return a URL served by fastify-static
- Unified handling of base64 vs URL responses from the Images API
- DALL-E quality mapped automatically (low/medium/high -> standard)
Local MinIO stack:
- docker-compose.yml at repo root with minio + minio-init service
that auto-creates the bucket and makes it publicly readable
- Default credentials: freedge / freedge123 (configurable)
- Console at :9001, API at :9000
- .env.example now points to the local stack by default
Static file serving:
- Register @fastify/static to serve ./uploads at /uploads/*
- Enables local fallback to return usable URLs to the frontend
- New PUBLIC_BASE_URL env var to build absolute URLs
TypeScript errors (21 -> 0):
- JWT typing via '@fastify/jwt' module augmentation (FastifyJWT
interface with payload + user) fixes all request.user.id errors
- Stripe constructor now passes required StripeConfig
- fastify.createCustomer guards checked on the helper itself for
proper TS narrowing (not on fastify.stripe)
- Remove 'done' arg from async onClose hook
- MinIO transport.agent + listFiles return type cast ignored
README:
- Add 'Stockage des fichiers' section explaining the two modes
- Updated setup instructions to start with docker compose up
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>