expria-backend/docs/ARCHITECTURE-backend.md
2026-04-26 03:09:13 +03:00

20 KiB

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
│   │   ├── stripe.ts        # POST /stripe/checkout, POST /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.

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.

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

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              Crée une session Stripe Checkout (nouveau abonnement)
POST   /plans/upgrade-prorata      Upgrade en cours d'abonnement (prorata Stripe)

Stripe

POST   /stripe/checkout            Crée une Checkout Session Stripe
POST   /stripe/webhook             Reçoit les events Stripe (checkout, invoice, deleted)

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