ordinarthur b0e9425ed5 feat(stripe): on-demand sync fallback when webhooks are missed
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>
2026-04-08 14:07:02 +02:00
2025-03-10 00:24:26 +01:00

Freedge

Freedge génère des recettes personnalisées à partir des ingrédients dictés à l'oral. Le son est transcrit (Whisper), une recette est générée (GPT-4o-mini) et une image optionnelle est produite (DALL-E 3).

Stack

  • Frontend : React 19 + Vite + TailwindCSS + ShadCN/UI + React Router
  • Backend : Fastify 4 + TypeScript + Prisma 5 + SQLite
  • IA : OpenAI (gpt-4o-mini-transcribe, gpt-4o-mini avec Structured Outputs, gpt-image-1)
  • Stockage : MinIO (S3-compatible) avec fallback local
  • Paiement : Stripe Checkout hébergé + Customer Portal + webhooks signés
  • Auth : JWT + Google OAuth

Structure

freedge/
├── backend/                 # TypeScript
│   ├── prisma/              # Schéma + migrations SQLite
│   ├── tsconfig.json
│   └── src/
│       ├── ai/              # prompts, recipe-generator, image-generator,
│       │                    #   transcriber, cost (logging tokens/USD)
│       ├── plugins/         # auth, ai, stripe, google-auth
│       ├── routes/          # auth, recipes, users
│       ├── types/           # fastify.d.ts (augmentation décorateurs)
│       ├── utils/           # env, storage (MinIO), email
│       └── server.ts
└── frontend/
    └── src/
        ├── api/             # Clients HTTP (auth, user, recipe)
        ├── components/      # UI shadcn + composants métier
        ├── hooks/           # useAuth, useMobile, useAudioRecorder
        ├── layouts/         # MainLayout
        └── pages/           # Home, Auth/*, Recipes/*, Profile, ResetPassword

Prérequis

  • Node.js ≥ 18
  • pnpm (recommandé) ou npm
  • Une clé API OpenAI
  • (Optionnel) Un serveur MinIO pour le stockage images/audio
  • (Optionnel) Un compte Resend pour les emails

Démarrage

# 1. Services (MinIO) — depuis la racine du projet
docker compose up -d
# → MinIO sur :9000 (API) + :9001 (console web, user freedge / pass freedge123)
# → le bucket 'freedge' est créé automatiquement et rendu lisible publiquement

# 2. Installation
cd backend && npm install
cd ../frontend && pnpm install

# 3. Variables d'environnement backend (.env dans backend/)
cp .env.example .env   # puis éditer OPENAI_API_KEY et JWT_SECRET
# Les défauts MinIO du .env.example pointent sur la stack docker-compose

# 4. Base de données
cd backend
npx prisma migrate dev
npx prisma generate

# 5. Lancement
npm run dev                                # backend (tsx watch) sur :3000
cd ../frontend && pnpm dev                 # frontend sur :5173

# Build production backend
cd backend && npm run build && npm start   # compile vers dist/ puis lance node dist/server.js
npm run typecheck                          # vérification TS sans build

Variables d'environnement backend

Variable Requis Description
DATABASE_URL URL Prisma (ex: file:./prisma/dev.db)
JWT_SECRET Clé JWT (≥ 32 caractères recommandé)
OPENAI_API_KEY Clé API OpenAI
PORT Port du serveur (défaut 3000)
CORS_ORIGINS Origines autorisées, séparées par virgule
FRONTEND_URL URL frontend pour les emails de reset
OPENAI_TEXT_MODEL Défaut gpt-4o-mini (recette)
OPENAI_TRANSCRIBE_MODEL Défaut gpt-4o-mini-transcribe
OPENAI_IMAGE_MODEL Défaut gpt-image-1 (requiert org vérifiée sur OpenAI)
OPENAI_IMAGE_FALLBACK_MODEL Défaut dall-e-3, utilisé si le principal échoue
OPENAI_IMAGE_QUALITY low / medium / high (défaut medium)
OPENAI_IMAGE_SIZE Défaut 1024x1024
ENABLE_IMAGE_GENERATION false pour désactiver totalement la génération d'image
OPENAI_MAX_RETRIES Retries SDK OpenAI (défaut 3)
OPENAI_TIMEOUT_MS Timeout par requête (défaut 60000)
STRIPE_SECRET_KEY Clé Stripe (pour créer les customers)
MINIO_ENDPOINT / MINIO_PORT / MINIO_USE_SSL / MINIO_ACCESS_KEY / MINIO_SECRET_KEY / MINIO_BUCKET Config MinIO ; fallback local ./uploads sinon
PUBLIC_BASE_URL URL publique du backend pour les fichiers locaux (défaut http://localhost:3000)
MINIO_ALLOW_SELF_SIGNED true pour autoriser TLS auto-signé (DEV uniquement)
RESEND_API_KEY Clé Resend pour les emails

Routes API (préfixe /)

Auth (/auth)

  • POST /auth/register — Inscription email + mot de passe
  • POST /auth/login — Connexion
  • POST /auth/google-auth — Connexion/inscription via Google OAuth

Utilisateurs (/users)

  • GET /users/profile — Profil courant (🔒)
  • PUT /users/profile — Mise à jour du nom (🔒)
  • PUT /users/change-password — Changement de mot de passe (🔒)
  • PUT /users/change-email — Changement d'email (🔒)
  • POST /users/forgot-password — Demande de réinitialisation
  • POST /users/reset-password — Réinitialisation avec token
  • DELETE /users/account — Suppression du compte (🔒)

Recettes (/recipes) — toutes 🔒

  • POST /recipes/create — Upload audio + transcription + génération
  • POST /recipes/create-stream — Version streaming SSE
  • GET /recipes/list — Liste les recettes de l'utilisateur
  • GET /recipes/:id — Détail d'une recette
  • DELETE /recipes/:id — Supprime une recette

Stripe (/stripe)

  • GET /stripe/plans — Liste publique des plans disponibles
  • GET /stripe/subscription — Statut d'abonnement de l'utilisateur 🔒
  • POST /stripe/checkout — Crée une Checkout Session (body: { plan }) 🔒
  • POST /stripe/portal — Ouvre le Customer Portal 🔒
  • POST /stripe/webhook — Receiver d'événements Stripe (signature vérifiée)

Divers

  • GET /health — Healthcheck

🔒 = nécessite un JWT Authorization: Bearer <token>

Stockage des fichiers (images & audio)

Deux modes, avec fallback automatique :

  1. MinIO (recommandé, lancé via docker compose up -d)

  2. Local (fallback si MinIO est indisponible)

    • Les fichiers sont écrits dans backend/uploads/{audio,recipes}/
    • Servis par Fastify sur GET /uploads/*
    • L'URL publique retournée au frontend est ${PUBLIC_BASE_URL}/uploads/recipes/xxx.png

Le frontend n'a pas à se soucier du mode : il reçoit une URL publique dans tous les cas.

Pipeline IA

Le module src/ai/ est isolé du reste du backend pour faciliter les itérations.

audio (multipart)
  └─→ saveAudioFile (MinIO ou local)
        └─→ transcribeAudio (gpt-4o-mini-transcribe, ~3s)
              └─→ generateRecipe (gpt-4o-mini, json_schema strict, ~3-5s)
                    ├─→ Recette structurée riche (titre, description,
                    │    inspiration, ingrédients avec notes, étapes
                    │    chronométrées, accord boisson, conseils)
                    └─→ generateRecipeImage (gpt-image-1 medium, ~6s)
                         (parallélisée avec la sérialisation JSON)

Caractéristiques :

  • Structured Outputs strict : la réponse JSON est validée côté OpenAI via json_schema. Plus jamais de JSON.parse qui casse.
  • Prompt système long et stable (~1500 tokens, persona "chef Antoine" avec règles d'originalité, de précision technique, et exemples few-shot). → Bénéficie du prompt caching automatique d'OpenAI (-50% sur la portion cachée à partir du 2ᵉ appel).
  • Retries + timeout au niveau du SDK OpenAI (configurables via env).
  • Logs de coût : chaque appel OpenAI émet un log structuré avec model, durationMs, costUsd, usage et cacheHit. Permet de suivre les coûts par utilisateur et de mesurer l'effet du cache.
  • Image best-effort : un échec de génération d'image ne casse pas la création de recette.

Stripe — configuration

1. Créer les produits et prix

Option A — script automatique (recommandé)

cd backend
# Mets juste STRIPE_SECRET_KEY dans .env, puis :
npm run stripe:setup              # affiche les IDs à copier
# ou :
npm run stripe:setup:write        # écrit directement dans backend/.env

Le script est idempotent (utilise des lookup_key sur les prix) : tu peux le relancer autant de fois que tu veux, il ne créera pas de doublons.

Option B — manuel, via le dashboard Stripe

  1. Crée deux produits récurrents :
    • Essentiel — 3€/mois
    • Premium — 5€/mois
  2. Note les Price IDs (commencent par price_...) et colle-les dans backend/.env.

2. Configurer le Customer Portal

Dans Settings → Billing → Customer Portal :

  • Active Cancel subscriptions (pour permettre l'annulation)
  • Active Switch plans et ajoute les 2 produits (pour permettre le changement)
  • Active Update payment methods
  • Dans "Branding", renseigne le nom Freedge et l'URL de support

3. Variables d'environnement backend

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_ID_ESSENTIAL=price_...
STRIPE_PRICE_ID_PREMIUM=price_...

4. Tester les webhooks en local

Installe la CLI Stripe : brew install stripe/stripe-cli/stripe.

stripe login
stripe listen --forward-to localhost:3000/stripe/webhook
# → affiche "whsec_..." à coller dans STRIPE_WEBHOOK_SECRET

Dans un autre terminal, tu peux simuler un événement :

stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger customer.subscription.deleted

5. Cartes de test

Utilise les numéros fournis par Stripe dans le dashboard test :

  • 4242 4242 4242 4242 → paiement réussi
  • 4000 0000 0000 9995 → carte déclinée

Sécurité

  • Helmet + rate-limit (100 req/min) activés
  • CORS whitelisté via CORS_ORIGINS
  • JWT signé, expiration 7 jours
  • Bcrypt (10 rounds) pour les mots de passe
  • Validation des variables d'environnement au démarrage

Licence

MIT

Description
No description provided
Readme 2.3 MiB
Languages
TypeScript 96.6%
CSS 1.7%
Shell 0.6%
Dockerfile 0.6%
JavaScript 0.3%
Other 0.2%