expria-backend/docs/ARCHITECTURE-backend.md
Hermann_Kitio 6671bac347 feat(billing): TD-13 webhook idempotency + Stripe Customer Portal + doc cleanup
- Table stripe_webhook_events + helpers isEventProcessed/markEventProcessed
- POST /stripe/customer-portal (auth + stripe_customer_id check)
- ARCHITECTURE-backend.md: suppression POST /plans/upgrade (duplication doc)
- TD-13 fermé dans TECH_DEBT-backend.md
- Tests: 261 → 278 verts (+17)
2026-04-26 04:15:46 +03:00

524 lines
20 KiB
Markdown

# ARCHITECTURE.md — Expria / Coach TCF Canada
> **Document de référence — Version 1.2**
> Ce document décrit l'architecture technique complète du projet.
> Toute décision architecturale majeure doit être documentée ici avant d'être implémentée.
> À lire conjointement avec PLANS_TARIFAIRES.md et PARCOURS_UTILISATEURS.md.
---
## 1. Vue d'ensemble
Expria est une application web SaaS de coaching TCF Canada, construite sur une architecture
frontend / backend séparés, avec une pièce dédiée pour la fonctionnalité temps réel (T2 EO live).
```
Utilisateur (navigateur)
┌─────────────────────────────┐
│ FRONTEND │
│ React + Vite │
│ Cloudflare Pages (gratuit) │
└─────────────┬───────────────┘
│ appels API REST
┌─────────────▼───────────────┐
│ BACKEND │
│ Hono.js (Node.js) │
│ Render (Frankfurt) │
│ — toutes les routes API │
│ — WebSocket proxy T2 EO │
└──────┬──────────────┬───────┘
│ │
┌──────▼──────┐ ┌────▼────────────────┐
│ Supabase │ │ APIs externes │
│ PostgreSQL │ │ — DeepSeek (EE/EO) │
│ Auth │ │ — Gemini (audio) │
│ Storage │ │ — Stripe (paiement)│
└─────────────┘ └─────────────────────┘
```
---
## 2. Décisions architecturales
### Pourquoi frontend séparé (React + Vite) et non Next.js
Next.js est un framework full-stack qui mélange frontend et backend dans le même projet.
Ce mélange a été la principale source de chaos dans la version précédente d'Expria :
régressions fréquentes, difficultés à tester, couplage fort entre affichage et logique métier.
React + Vite est un frontend pur — il ne fait qu'afficher et appeler des API.
Il ne connaît pas Supabase, ne connaît pas Gemini, ne connaît pas Stripe.
Il reçoit des données depuis le backend et les affiche. C'est tout.
### Pourquoi Hono.js pour le backend
Hono est un framework backend moderne, léger, TypeScript natif, conçu pour Node.js.
Il offre une structure claire de routes, middlewares, et contrôleurs — similaire à Express
mais plus moderne et mieux typé. Il tourne sur Render sans configuration particulière.
### Pourquoi Render pour le backend (et non Cloudflare Workers)
La T2 EO live nécessite une connexion WebSocket persistante entre le serveur et Gemini Live API,
pouvant durer plusieurs minutes. Cloudflare Workers impose une durée d'exécution maximale
incompatible avec cette contrainte. Render supporte nativement les connexions WebSocket longue durée.
### Pourquoi Cloudflare Pages pour le frontend
Le frontend est un ensemble de fichiers statiques (HTML, CSS, JS). Cloudflare Pages les distribue
depuis un CDN mondial — latence minimale pour les utilisateurs en Algérie, Maroc, Cameroun.
Tier gratuit, déploiement automatique depuis GitHub.
### Pourquoi Supabase est conservé
Supabase fournit trois services critiques déjà en production :
- Authentification complète (email, OAuth Google/Apple, sessions JWT)
- Base de données PostgreSQL avec Row Level Security
- Stockage de fichiers (enregistrements audio EO)
Remplacer Supabase par une Render DB nue demanderait de recoder l'authentification
et les politiques de sécurité de zéro — un coût disproportionné sans bénéfice.
---
## 3. Structure des dépôts
Deux dépôts GitHub séparés :
```
expria-frontend/ → React + Vite
expria-backend/ → Hono.js
```
Supabase est géré via son propre dashboard et CLI (migrations SQL versionnées).
### Pourquoi deux dépôts séparés
- Claude Code travaille sur un périmètre clair : frontend OU backend, jamais les deux en même temps
- Une modification frontend ne peut pas casser le backend par accident
- Déploiements indépendants : on peut déployer le frontend sans toucher au backend
---
## 4. Structure des dossiers
### Frontend — expria-frontend/
```
expria-frontend/
├── public/ # Assets statiques (favicon, images)
├── src/
│ ├── api/ # Fonctions d'appel API (une par domaine)
│ │ ├── auth.ts # Appels auth (login, logout, register)
│ │ ├── simulations.ts # Appels simulations (créer, récupérer)
│ │ ├── corrections.ts # Appels corrections (rapport, score)
│ │ ├── plans.ts # Appels plans (upgrade, status)
│ │ └── stripe.ts # Appels paiement (checkout, prorata)
│ ├── components/ # Composants React réutilisables
│ │ ├── ui/ # Composants génériques (Button, Modal, Badge)
│ │ ├── simulation/ # Composants spécifiques aux simulations
│ │ ├── rapport/ # Composants d'affichage des rapports
│ │ ├── dashboard/ # Composants du dashboard utilisateur
│ │ └── paywall/ # Composants de blocage / upgrade
│ ├── pages/ # Pages de l'application (une par route)
│ │ ├── Home.tsx # Page d'accueil (visiteur non connecté)
│ │ ├── Login.tsx # Connexion / inscription
│ │ ├── Dashboard.tsx # Dashboard utilisateur (tous plans)
│ │ ├── Simulation.tsx # Interface de simulation (EE + EO)
│ │ ├── Rapport.tsx # Affichage d'un rapport de correction
│ │ ├── Methodologie.tsx # Page méthodologie
│ │ ├── Tarifs.tsx # Page tarifaire
│ │ └── T2Live.tsx # Interface T2 EO live (Premium uniquement)
│ ├── hooks/ # Hooks React personnalisés
│ │ ├── useAuth.ts # Gestion de l'authentification
│ │ ├── usePlan.ts # Lecture du plan actuel + permissions
│ │ ├── useSimulation.ts # Logique de simulation
│ │ └── useT2Live.ts # Connexion WebSocket T2 live
│ ├── lib/
│ │ ├── access.ts # Source de vérité des permissions par plan
│ │ ├── supabase.ts # Client Supabase (auth uniquement côté frontend)
│ │ └── constants.ts # Constantes globales (URLs, config)
│ ├── types/ # Types TypeScript partagés
│ │ ├── plans.ts # Types Plan, Permission
│ │ ├── simulation.ts # Types Simulation, Tache, Production
│ │ └── rapport.ts # Types Rapport, Critere, Score
│ ├── App.tsx # Router principal
│ └── main.tsx # Point d'entrée
├── .env.example # Variables d'environnement (sans valeurs)
├── vite.config.ts
├── tsconfig.json
└── package.json
```
### Backend — expria-backend/
```
expria-backend/
├── src/
│ ├── routes/ # Définition des routes API (une par domaine)
│ │ ├── auth.ts # POST /auth/verify-token
│ │ ├── simulations.ts # POST /simulations, GET /simulations/:id
│ │ ├── corrections.ts # POST /corrections/ee, POST /corrections/eo
│ │ ├── plans.ts # GET /plans/status, POST /plans/upgrade-prorata
│ │ ├── stripe.ts # POST /stripe/checkout, /stripe/customer-portal, /stripe/webhook
│ │ └── t2live.ts # WebSocket /t2/live (T2 EO proxy Gemini)
│ ├── controllers/ # Logique métier (une par domaine)
│ │ ├── simulationController.ts
│ │ ├── correctionController.ts
│ │ ├── planController.ts
│ │ ├── stripeController.ts
│ │ └── t2LiveController.ts
│ ├── middleware/ # Middlewares Hono
│ │ ├── auth.ts # Vérification JWT Supabase
│ │ ├── plan.ts # Vérification des permissions par plan
│ │ └── rateLimit.ts # Limitation des appels API
│ ├── lib/
│ │ ├── supabase.ts # Client Supabase admin (service role)
│ │ ├── deepseek.ts # Client DeepSeek (corrections EE/EO)
│ │ ├── gemini.ts # Client Gemini (transcription audio)
│ │ ├── stripe.ts # Client Stripe
│ │ └── access.ts # Copie de la source de vérité des plans
│ ├── types/ # Types TypeScript partagés
│ └── index.ts # Point d'entrée Hono
├── .env.example
├── tsconfig.json
└── package.json
```
---
## 5. Tables Supabase
### Table : profiles
Créée automatiquement à l'inscription. Liée à `auth.users`.
```sql
CREATE TABLE profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
email TEXT NOT NULL,
plan TEXT NOT NULL DEFAULT 'free'
CHECK (plan IN ('free', 'standard', 'premium')),
simulations_used INTEGER NOT NULL DEFAULT 0,
stripe_customer_id TEXT,
stripe_subscription_id TEXT,
plan_expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Trigger : mise à jour automatique de updated_at
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN NEW.updated_at = NOW(); RETURN NEW; END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER profiles_updated_at
BEFORE UPDATE ON profiles
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- RLS : chaque utilisateur ne voit que son propre profil
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Utilisateur voit son profil"
ON profiles FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Utilisateur modifie son profil"
ON profiles FOR UPDATE
USING (auth.uid() = id);
```
### Table : productions
Enregistrement de chaque simulation soumise pour correction.
```sql
CREATE TABLE productions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
tache TEXT NOT NULL
CHECK (tache IN ('EE_T1','EE_T2','EE_T3','EO_T1','EO_T3','EO_T2_LIVE')),
mode TEXT NOT NULL
CHECK (mode IN ('entrainement', 'examen')),
contenu TEXT, -- texte pour EE, transcript pour EO
audio_url TEXT, -- URL Supabase Storage (EO uniquement)
score NUMERIC(4,1), -- score /20
nclc INTEGER, -- niveau NCLC estimé
rapport JSONB, -- rapport complet (critères, erreurs, tips)
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index pour les requêtes fréquentes
CREATE INDEX productions_user_id_idx ON productions(user_id);
CREATE INDEX productions_created_at_idx ON productions(created_at DESC);
-- RLS
ALTER TABLE productions ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Utilisateur voit ses productions"
ON productions FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Utilisateur crée ses productions"
ON productions FOR INSERT
WITH CHECK (auth.uid() = user_id);
```
### Table : pattern_analyses
Résultats de l'analyse des patterns (Premium — 5 dernières productions).
```sql
CREATE TABLE pattern_analyses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
productions_ids UUID[] NOT NULL, -- IDs des 5 productions analysées
patterns JSONB NOT NULL, -- erreurs récurrentes détectées
exercises JSONB NOT NULL, -- exercices long terme générés
preparation_index INTEGER, -- indice de préparation 0-100
created_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE pattern_analyses ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Utilisateur voit ses analyses"
ON pattern_analyses FOR SELECT
USING (auth.uid() = user_id);
```
---
## 6. Routes API backend
### Authentification
```
POST /auth/verify-token Vérifie le JWT Supabase, retourne le profil + plan
```
### Simulations
```
POST /simulations Crée une simulation, vérifie les quotas selon le plan
GET /simulations/:id Récupère une simulation par ID
GET /simulations Liste les simulations de l'utilisateur connecté
```
### Corrections
```
POST /corrections/ee Soumet une production EE pour correction (DeepSeek)
POST /corrections/eo Soumet une production EO pour correction (Gemini)
```
### Plans
```
GET /plans/status Retourne le plan actuel + permissions de l'utilisateur
POST /plans/upgrade-prorata Upgrade en cours d'abonnement (prorata Stripe — preview du montant)
```
### Stripe
```
POST /stripe/checkout Crée une Checkout Session Stripe (nouveau abonnement)
POST /stripe/customer-portal Crée une Billing Portal Session (gestion abonnement self-service)
POST /stripe/webhook Reçoit les events Stripe (checkout, invoice, deleted) — idempotent (TD-13 résolu Sprint 5a)
```
### T2 EO Live
```
WS /t2/live WebSocket — proxy Gemini Live API (Premium uniquement)
```
---
## 7. Flux de données clés
### Flux : Simulation EE (mode entraînement)
```
1. Frontend → POST /simulations (créer la simulation, vérifier quota)
2. Backend → Supabase profiles (vérifier plan + simulations_used)
3. Backend → POST /corrections/ee (envoyer à DeepSeek)
4. DeepSeek → Backend (rapport JSON)
5. Backend → Supabase productions (enregistrer production + rapport)
6. Backend → Frontend (retourner le rapport)
7. Frontend → Afficher selon le plan (rapport complet ou flouté)
```
### Flux : Paiement nouveau abonnement
```
1. Frontend → POST /stripe/checkout (créer Checkout Session)
2. Backend → Stripe API (session avec price_id)
3. Stripe → Frontend (redirect vers page paiement Stripe)
4. Utilisateur paie
5. Stripe → POST /stripe/webhook (checkout.session.completed)
6. Backend → Supabase profiles (plan = 'standard' ou 'premium')
7. Frontend → Redirect dashboard (plan mis à jour)
```
### Flux : Upgrade Standard → Premium (prorata)
```
1. Frontend → POST /plans/upgrade-prorata
2. Backend → Stripe API (subscription.update + preview invoice)
3. Backend → Frontend (montant prorata calculé)
4. Frontend → Confirmation utilisateur (affiche le montant exact)
5. Frontend → POST /plans/upgrade-prorata (confirmation)
6. Backend → Stripe API (subscription.update confirmé)
7. Stripe → POST /stripe/webhook (invoice.paid)
8. Backend → Supabase profiles (plan = 'premium')
9. Frontend → Dashboard Premium (accès immédiat)
```
### Flux : T2 EO Live (WebSocket)
```
1. Frontend → WS /t2/live (connexion WebSocket, JWT vérifié)
2. Backend → Vérification plan (premium uniquement)
3. Backend → WS Gemini Live API (ouverture connexion avec GEMINI_API_KEY)
4. Frontend → Backend → Gemini (audio candidat en temps réel)
5. Gemini → Backend → Frontend (audio examinateur IA en temps réel)
6. Fin dialogue
7. Backend → POST Gemini REST (évaluation de la session)
8. Backend → Supabase productions (enregistrement production + rapport)
9. Backend → Frontend (rapport complet)
```
---
## 8. Variables d'environnement
### Frontend (.env)
```
VITE_API_URL=https://api.expria.app # URL du backend Render
VITE_SUPABASE_URL=https://xxx.supabase.co
VITE_SUPABASE_ANON_KEY=xxx # Clé publique uniquement
```
### Backend (.env)
```
# Supabase
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_SERVICE_ROLE_KEY=xxx # Clé privée — ne jamais exposer
# APIs
DEEPSEEK_API_KEY=xxx
GEMINI_API_KEY=xxx # Ne jamais exposer côté frontend
# Stripe
STRIPE_SECRET_KEY=xxx # Clé privée Stripe
STRIPE_WEBHOOK_SECRET=xxx # Secret webhook Stripe
STRIPE_PRICE_STANDARD=price_xxx # price_id plan Standard
STRIPE_PRICE_PREMIUM=price_xxx # price_id plan Premium
# Config
PORT=3000
APP_URL=https://expria.app
API_URL=https://api.expria.app
NODE_ENV=production
```
> **Règle absolue :** aucune clé privée (SUPABASE_SERVICE_ROLE_KEY, GEMINI_API_KEY,
> STRIPE_SECRET_KEY) ne doit jamais se retrouver dans le frontend ou dans un commit Git.
---
## 9. Déploiement
### Hébergement Git — GitHub
- Plateforme : github.com
- Dépôt frontend : `https://github.com/germannoff/expria-frontend`
- Dépôt backend : `https://github.com/germannoff/expria-backend`
- Note : compte GitHub réactivé le 17 avril 2026 après restriction OFAC levée
- Auto-deploy : disponible via Render (connecté à GitHub)
### Frontend — Cloudflare Pages
- Source : dépôt GitHub `expria-frontend`
- Build command : `npm run build`
- Output directory : `dist`
- Domaine : `expria.app` (DNS pointé depuis Vercel vers Cloudflare Pages)
- Déploiement : **manuel via Cloudflare Pages CLI**
```bash
# Commande de déploiement frontend
npm run build
npx wrangler pages deploy dist --project-name=expria
```
### Backend — Render
- Source : dépôt GitHub `expria-backend`
- Type : Web Service (Node.js)
- Région : Frankfurt (EU) — proximité utilisateurs Afrique du Nord
- Build command : `npm run build`
- Start command : `npm start`
- Domaine : `api.expria.app` (certificat SSL actif)
- URL Render : `https://expria-backend.onrender.com` (alias)
- WebSocket : activé nativement sur Render
- Déploiement : **automatique à chaque push sur main (GitHub → Render)**
### Base de données — Supabase
- Région : Frankfurt (EU)
- Migrations : versionnées dans `supabase/migrations/`
- Déploiement : `supabase db push` (manuel, après validation)
### Procédure de déploiement complète
```
1. Tester localement (npm run test — tous les tests verts)
2. Rejouer le Golden Dataset
3. Commit + push sur GitHub (branche main)
4. Backend : auto-deploy Render déclenché automatiquement
5. Déployer le frontend : npm run build && npx wrangler pages deploy dist
6. Vérifier les URLs de production (expria.app + api.expria.app)
7. Rejouer le Smoke Test (Groupe Z du Golden Dataset)
```
---
## 10. Règles de développement
### Règle 1 — Séparation stricte
Le frontend ne contient aucune logique métier.
Il appelle le backend et affiche ce qu'il reçoit.
Toute vérification de plan, de quota, de droit d'accès se fait côté backend.
### Règle 2 — Source de vérité unique des plans
`lib/access.ts` existe dans les deux dépôts (frontend et backend).
Le fichier doit être identique dans les deux.
Toute modification des plans tarifaires met à jour ce fichier en premier,
dans les deux dépôts, avant tout autre changement de code.
### Règle 3 — Jamais plus de 3 fichiers touchés par session Claude
Si une modification nécessite de toucher plus de 3 fichiers,
elle doit être découpée en plusieurs sessions avec validation intermédiaire.
### Règle 4 — Plan avant code
Claude Code ne commence jamais à coder sans avoir d'abord produit
un plan détaillé (fichiers impactés, risques, étapes).
Le plan est validé par Hermann avant l'exécution.
### Règle 5 — Tests manuels après chaque session
Après chaque session Claude Code, rejouer le golden dataset
(voir GOLDEN_DATASET.md) avant de passer à la session suivante.
### Règle 6 — Variables d'environnement
Aucune valeur de variable d'environnement n'est jamais écrite en dur dans le code.
Toujours lire depuis `process.env` (backend) ou `import.meta.env` (frontend).