- 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)
524 lines
20 KiB
Markdown
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).
|