Merge pull request 'docs: référence unique de toutes les variables d'environnement' (#20) from docs/environment-variables into feat/merge-admin-into-web
Some checks failed
CI / lint (push) Successful in 2m11s
CI / build (push) Successful in 2m39s
CI / e2e (push) Successful in 12m29s
CI / test (push) Failing after 23m54s
CI / intent-service-test (push) Successful in 28m5s

Reviewed-on: #20
This commit is contained in:
kyuno 2026-08-29 21:27:38 +02:00
commit 371ed5593f
2 changed files with 257 additions and 1 deletions

View file

@ -56,7 +56,7 @@ cp apps/api/.env.example apps/api/.env
cp apps/web/.env.example apps/web/.env cp apps/web/.env.example apps/web/.env
``` ```
Puis **édite ces deux `.env`** pour renseigner de vrais `POSTGRES_USER`/`POSTGRES_PASSWORD` Puis **édite ces `.env`** pour renseigner de vrais `POSTGRES_USER`/`POSTGRES_PASSWORD`
(et la `DATABASE_URL` correspondante dans `apps/api/.env`) : les fichiers `.env.example` (et la `DATABASE_URL` correspondante dans `apps/api/.env`) : les fichiers `.env.example`
ne contiennent volontairement aucun identifiant réel (juste `changeme`), et ne contiennent volontairement aucun identifiant réel (juste `changeme`), et
`docker-compose.yml` refuse de démarrer tant que `POSTGRES_USER`/`PASSWORD`/`DB` ne `docker-compose.yml` refuse de démarrer tant que `POSTGRES_USER`/`PASSWORD`/`DB` ne
@ -64,6 +64,13 @@ sont pas définis dans `.env` — pas de valeur par défaut en dur dans les fich
Même règle pour `apps/api/.env` : `JWT_SECRET` est **requis, sans défaut** (génère le Même règle pour `apps/api/.env` : `JWT_SECRET` est **requis, sans défaut** (génère le
tien, voir le commentaire dans `apps/api/.env.example`). tien, voir le commentaire dans `apps/api/.env.example`).
> **Toutes les variables d'environnement** (lesquelles poser, dans quel fichier,
> obligatoire ou non, à quoi elles servent, quels secrets doivent correspondre)
> sont listées et expliquées dans **[specs/environment.md](specs/environment.md)**.
> Le service NLP (`services/tech-step-intent-service`) et, si tu le lances, le
> worker LLM (`services/tech-step-llm-worker`) ont chacun leur propre `.env` à
> copier — voir ce document.
Si tu comptes lancer `pnpm --filter api test` (voir [Qualité / Tests](#qualité--tests)), Si tu comptes lancer `pnpm --filter api test` (voir [Qualité / Tests](#qualité--tests)),
crée aussi `apps/api/.env.test` — voir la section dédiée plus bas. crée aussi `apps/api/.env.test` — voir la section dédiée plus bas.

249
specs/environment.md Normal file
View file

@ -0,0 +1,249 @@
# Variables d'environnement
Référence unique de **toutes** les variables d'environnement de l'appli :
lesquelles poser, dans quel fichier, obligatoire ou non, et à quoi elles
servent.
La **source de vérité** de chaque variable reste son schéma de validation
(`apps/api/src/config/env.ts`, `services/tech-step-llm-worker/src/config.ts`,
`services/tech-step-intent-service/intent_service/config.py`) : au démarrage,
un secret requis manquant fait **échouer immédiatement** le process plutôt que
de laisser une erreur obscure survenir plus tard. Ce document résume ces
schémas et les relie entre eux.
---
## Principes
- **Aucun secret n'est commité.** Les fichiers `*.env.example` ne contiennent
que des `changeme` ; les vrais `.env` sont git-ignorés et à créer par copie.
- **Générer un secret** (32 caractères minimum) :
```bash
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
```
- **Deux workflows, deux jeux de fichiers :**
| Workflow | Fichiers lus |
|---|---|
| Dev natif (`pnpm dev:api` / `pnpm dev:web` + services lancés à la main) | `apps/api/.env`, `apps/web/.env`, `services/*/.env` |
| `pnpm --filter api test` | `apps/api/.env.test` (chargé à la place de `.env` via `NODE_ENV=test`) |
| Docker Compose (`docker compose …`, prod/review) | `.env` à la racine uniquement — Compose injecte le reste en `environment:` |
- **Secrets partagés :** plusieurs variables portent le même secret des deux
côtés d'un lien (API ↔ service). Elles **doivent avoir la même valeur**
voir [Secrets à faire correspondre](#secrets-à-faire-correspondre).
---
## Fichiers `.env`
| Fichier | Créé par | Utilisé pour | Modèle |
|---|---|---|---|
| `.env` (racine) | `cp .env.example .env` | **Docker Compose uniquement** (provisionne Postgres + injecte les vars des services) | `.env.example` |
| `apps/api/.env` | `cp apps/api/.env.example apps/api/.env` | API en dev natif (`pnpm dev:api`) | `apps/api/.env.example` |
| `apps/api/.env.test` | `cp apps/api/.env.test.example apps/api/.env.test` | `pnpm --filter api test` (base **séparée** de la dev) | `apps/api/.env.test.example` |
| `apps/web/.env` | `cp apps/web/.env.example apps/web/.env` | Front en dev natif — lu **au build** par Vite | `apps/web/.env.example` |
| `services/tech-step-intent-service/.env` | `cp …/.env.example …/.env` | Service NLP en dev natif | idem |
| `services/tech-step-llm-worker/.env` | `cp …/.env.example …/.env` | Worker LLM en dev natif (**optionnel**) | idem |
| `services/tech-step-llm-worker/.env.test` | `cp …/.env.test.example …/.env.test` | Tests Mocha du worker | idem |
---
## Postgres — `.env` racine (Docker Compose)
Provisionnent le conteneur `postgres` de `docker-compose.yml`. En dev natif,
ces valeurs ne sont lues que par Compose pour lancer la base ; l'API, elle,
lit `DATABASE_URL` dans `apps/api/.env` (qui doit refléter ces identifiants).
| Variable | Obligatoire | Défaut | Rôle |
|---|---|---|---|
| `POSTGRES_USER` | **oui** (Compose refuse de démarrer sans) | — | Utilisateur du conteneur Postgres |
| `POSTGRES_PASSWORD` | **oui** | — | Mot de passe |
| `POSTGRES_DB` | **oui** | — | Nom de la base |
| `POSTGRES_PORT` | non | `5432` | Port hôte exposé. Passer à `5433` si un Postgres natif occupe déjà `5432` (adapter aussi `DATABASE_URL`) |
---
## API — `apps/api/.env` (dev natif) / injecté par Compose
Schéma : `apps/api/src/config/env.ts`.
| Variable | Obligatoire | Défaut | Rôle |
|---|---|---|---|
| `NODE_ENV` | non | `development` | `development` \| `test` \| `production`. `test` déclenche des comportements dédiés (coût argon2 réduit, chargement de `.env.test`). |
| `PORT` | non | `3000` | Port d'écoute HTTP de l'API. |
| `DATABASE_URL` | **de fait oui** (Prisma en a besoin) | — | Chaîne de connexion Postgres. Doit refléter les identifiants du `.env` racine. En Compose elle est construite automatiquement et cible l'hôte `postgres:5432`. |
| `JWT_SECRET` | **oui, sans défaut** | — | Signe/vérifie le JWT de session utilisateur. ≥ 32 caractères. |
| `JWT_EXPIRES_IN` | non | `7d` | Durée de validité du JWT (format `jsonwebtoken`). |
| `AUTH_COOKIE_NAME` | non | `session` | Nom du cookie httpOnly de session utilisateur. |
| `CORS_ORIGIN` | non | `http://localhost:5173` | Origine autorisée par CORS — doit correspondre à l'URL de `apps/web`. En Compose, le front est servi par le **même** conteneur : pas de CORS cross-origin. |
| `COOKIE_SECURE` | non | *(non posé → `NODE_ENV === "production"`)* | Force l'attribut `Secure` du cookie de session. Mettre `false` **uniquement** si le déploiement est en HTTP nu (sans TLS devant) : sinon le cookie n'est jamais renvoyé et toute requête authentifiée renvoie 401 après un login pourtant réussi. |
| `FRONTEND_DIST_DIR` | non | — | Chemin absolu vers `apps/web/dist` à servir avec l'API. Posé **uniquement** dans l'image Docker de prod ; laissé vide en dev natif (c'est le serveur Vite de `pnpm dev:web` qui sert le front). |
### Lien API ↔ service NLP (`tech-step-intent-service`)
| Variable | Obligatoire | Défaut | Rôle |
|---|---|---|---|
| `INTENT_SERVICE_BASE_URL` | non | `http://localhost:8000` | URL du service NLP. Compose la remplace par le nom de service réseau (`http://tech-step-intent-service:8000`). |
| `INTENT_SERVICE_SECRET` | **oui, sans défaut** | — | Secret partagé (header `X-Intent-Service-Secret`). **Dépendance cœur** : sans le service NLP joignable, aucune technique de cuisine n'est détectée à l'enregistrement/l'aperçu d'une recette. Doit être **identique** à celui du service ([voir plus bas](#service-nlp--servicestech-step-intent-serviceenv)). |
### Surface d'administration `/admin/*` (optionnelle)
L'UI admin est servie par `apps/web` sous `/admin/*` (même origine que le
reste du front — pas de CORS dédié), mais avec une **auth totalement
séparée** de celle des utilisateurs.
| Variable | Obligatoire | Défaut | Rôle |
|---|---|---|---|
| `ADMIN_JWT_SECRET` | non *(mais `/admin/*` renvoie 401 tant qu'absent)* | — | Signe/vérifie le JWT de session admin. **Doit être ≠ `JWT_SECRET`** pour qu'un token utilisateur ne puisse jamais être rejoué contre `/admin/*`. ≥ 32 caractères. |
| `ADMIN_COOKIE_NAME` | non | `admin_session` | Nom du cookie httpOnly de session admin — doit différer de `AUTH_COOKIE_NAME` pour que les deux sessions coexistent dans un même navigateur. |
| `ADMIN_INITIAL_EMAIL` | non | — | Valeurs lues **uniquement** par `apps/api/src/scripts/create-admin.ts` quand ses flags `--email` / `--password` / `--name` sont omis (bootstrap du premier admin). Jamais lues par le serveur. |
| `ADMIN_INITIAL_PASSWORD` | non | — | idem |
| `ADMIN_INITIAL_NAME` | non | — | idem |
### Lien API ↔ worker LLM (`tech-step-llm-worker`, optionnel)
| Variable | Obligatoire | Défaut | Rôle |
|---|---|---|---|
| `INTERNAL_WORKER_SECRET` | non *(mais `/internal/tech-steps/*` renvoie 401 tant qu'absent)* | — | Secret partagé (header `X-Internal-Worker-Secret`) pour les appels du worker LLM vers l'API. ≥ 32 caractères. Job de fond **optionnel** : un déploiement qui ne lance pas le worker n'en a pas besoin. Doit être **identique** à celui du worker. |
---
## Front — `apps/web/.env`
Vite n'expose au code client **que** les variables préfixées `VITE_`, et les
inline **au build** (pas de lecture à l'exécution).
| Variable | Obligatoire | Défaut | Rôle |
|---|---|---|---|
| `VITE_API_URL` | non | `""` (même origine) | Base URL de l'API pour `fetch` (`apiClient` **et** `adminApiClient`). En dev natif : `http://localhost:3000`. Vide = même origine, correct derrière un reverse-proxy partagé ou dans l'image Docker mono-conteneur. |
*(La version affichée dans l'UI (`__APP_VERSION__`) vient de `package.json`
via `vite.config.ts`, ce n'est pas une variable d'environnement.)*
---
## Service NLP — `services/tech-step-intent-service/.env`
Schéma : `intent_service/config.py` (`pydantic-settings`). En Docker, Compose
injecte les variables ; en dev natif, elles viennent de ce `.env`.
| Variable | Obligatoire | Défaut | Rôle |
|---|---|---|---|
| `INTENT_SERVICE_SECRET` | **oui, sans défaut** (le service refuse de démarrer sans) | — | Secret attendu sur `X-Intent-Service-Secret` de chaque requête (sauf `GET /health`). Doit être **identique** à `INTENT_SERVICE_SECRET` côté API. |
| `LOG_LEVEL` | non | `INFO` | Niveau du logging JSON structuré. À `INFO`, chaque appel `/v1/process` journalise son input/output (locale, texte, entités, intent, score). |
Le **port** n'est pas une variable d'env : il est passé à `uvicorn` en
argument (`--port 8000`).
---
## Worker LLM — `services/tech-step-llm-worker/.env` (optionnel)
Schéma : `src/config.ts`. Nécessaire **uniquement** hors Docker Compose
(Compose pose lui-même `API_BASE_URL` / `INTERNAL_WORKER_SECRET`).
| Variable | Obligatoire | Défaut | Rôle |
|---|---|---|---|
| `INTERNAL_WORKER_SECRET` | **oui, sans défaut** | — | Doit être **identique** à `INTERNAL_WORKER_SECRET` côté API. ≥ 32 caractères. |
| `API_BASE_URL` | non | `http://app:3000` | Base URL de l'API. Le défaut est le nom d'hôte Compose du service `app` ; à surcharger (`http://localhost:3000`) pour un `pnpm dev:api` local. |
| `TECH_STEP_WORKER_CRON` | non | `0 3 * * 0` (dimanche 03:00) | Expression cron (syntaxe `node-cron`) de réveil du worker. Valeur provisoire à calibrer une fois déployé. |
| `TECH_STEP_LLM_MODEL_URI` | non | `hf:Qwen/Qwen2.5-1.5B-Instruct-GGUF:Q4_K_M` | URI `hf:<repo>:<quant>` du modèle GGUF à télécharger. |
| `TECH_STEP_LLM_MODEL_PATH` | non | — | Chemin local explicite vers un GGUF, court-circuite le téléchargement HF ci-dessus (offline / éviter un download en plein déploiement). |
| `TECH_STEP_WORKER_LOCALE` | non | `fr` | Locale échantillonnée par le job `audit-low-confidence`. |
| `TECH_STEP_WORKER_BATCH_LIMIT` | non | `50` | Borne (`?limit=`) sur le nombre d'items traités par run — plafonne le coût d'inférence LLM d'une exécution planifiée. |
| `RUN_ONCE` | non | *(non posé → `false`)* | `true` = lance les deux jobs une fois puis quitte, au lieu de démarrer la boucle cron (run manuel / CI). Comparaison de chaîne stricte : `"false"` vaut bien `false`. |
`services/tech-step-llm-worker/.env.test` ne contient qu'un
`INTERNAL_WORKER_SECRET` bidon (les tests du worker mockent tous les appels
HTTP vers l'API) — juste là pour satisfaire le schéma à l'import.
---
## Compose — `.env` racine, variables spécifiques
| Variable | Obligatoire | Défaut | Rôle |
|---|---|---|---|
| `APP_PORT` | non | `3000` | Port hôte du conteneur `app` (API + front construit) dans `docker-compose.yml`. |
| `COOKIE_SECURE` | non | *(non posé)* | Voir la table API — `${COOKIE_SECURE:-}` dans Compose, à mettre à `false` seulement pour un déploiement HTTP nu. |
| `TECH_STEP_WORKER_CRON` | non | `0 3 * * 0` | Passé au service `tech-step-llm-worker` de Compose. |
Le `.env` racine porte aussi `JWT_SECRET`, `INTENT_SERVICE_SECRET`,
`INTERNAL_WORKER_SECRET`, `ADMIN_JWT_SECRET`, `ADMIN_INITIAL_*` : en Compose,
c'est **là** qu'ils sont posés (et non dans `apps/api/.env`, jamais copié dans
l'image).
---
## Secrets à faire correspondre
Un même secret doit porter la **même valeur** aux deux bouts :
| Secret | Bout A | Bout B | Si absent / divergent |
|---|---|---|---|
| `INTENT_SERVICE_SECRET` | `apps/api/.env` | `services/tech-step-intent-service/.env` | Le service NLP refuse chaque requête → **plus aucune détection de technique** (dépendance cœur). |
| `INTERNAL_WORKER_SECRET` | `apps/api/.env` | `services/tech-step-llm-worker/.env` | `/internal/tech-steps/*` renvoie 401 → le worker LLM ne peut rien faire (job optionnel). |
| Identifiants Postgres | `.env` racine (`POSTGRES_*`) | `apps/api/.env` (`DATABASE_URL`) | `P1000 Authentication failed`. |
Contraintes supplémentaires :
- `ADMIN_JWT_SECRET` **≠** `JWT_SECRET` (isolation des sessions admin/utilisateur).
- `ADMIN_COOKIE_NAME` **≠** `AUTH_COOKIE_NAME`.
- `apps/api/.env.test``DATABASE_URL` doit pointer une base **différente** de
`apps/api/.env` (la suite `TRUNCATE` tout avant chaque test ; un garde-fou
refuse de tourner si l'URL ne contient ni `test` ni `ci`).
---
## Mise en route rapide
### Dev natif
```bash
cp .env.example .env # Postgres (Compose)
cp apps/api/.env.example apps/api/.env # API
cp apps/web/.env.example apps/web/.env # front
cp services/tech-step-intent-service/.env.example \
services/tech-step-intent-service/.env # service NLP
```
Puis éditer :
1. `.env` — vrais `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB`.
2. `apps/api/.env``DATABASE_URL` avec ces mêmes identifiants ; un
`JWT_SECRET` généré ; le **même** `INTENT_SERVICE_SECRET` que…
3. `services/tech-step-intent-service/.env` — …ce fichier.
4. `apps/web/.env``VITE_API_URL=http://localhost:3000` (déjà le défaut du
modèle).
Optionnel — pour l'admin : ajouter `ADMIN_JWT_SECRET` (≥ 32 c, ≠ `JWT_SECRET`)
dans `apps/api/.env`, puis créer un admin :
```bash
pnpm --filter api exec tsx src/scripts/create-admin.ts \
--email=ops@example.com --password='…' --name='Ops'
```
Optionnel — pour le worker LLM : ajouter le **même** `INTERNAL_WORKER_SECRET`
dans `apps/api/.env` **et** `services/tech-step-llm-worker/.env`.
### Tests `apps/api`
```bash
cp apps/api/.env.test.example apps/api/.env.test
```
Éditer `DATABASE_URL` → base **dédiée** (ex. `batchcooking_test`), même
`JWT_SECRET` / `INTENT_SERVICE_SECRET` que d'habitude. Le service NLP doit
tourner. Détails : `apps/api/.env.test.example`.
### Docker Compose
Un seul fichier : `.env` à la racine. Y renseigner `POSTGRES_*`, `JWT_SECRET`,
`INTENT_SERVICE_SECRET`, et — si besoin — `ADMIN_JWT_SECRET`,
`INTERNAL_WORKER_SECRET`, `COOKIE_SECURE=false` (HTTP nu), `APP_PORT`.
```bash
docker compose up -d postgres # dev : Postgres seul
docker compose up -d --build # stack complète (review/prod)
```