# 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).