# 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::` 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) ```