diff --git a/.gitignore b/.gitignore
index 872d5f6..c2e1081 100644
--- a/.gitignore
+++ b/.gitignore
@@ -141,3 +141,6 @@ dist
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vite/
+
+# Project docs not meant to be committed
+Projet batch cooking.pdf
diff --git a/README.md b/README.md
index d75de94..1532820 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,9 @@ hors du repo).
# Base de données Postgres locale
docker compose up -d
+# Applique le schéma (première fois / après un changement de prisma/schema.prisma)
+pnpm --filter api exec prisma migrate dev
+
# Backend (http://localhost:3000)
pnpm dev:api
@@ -65,6 +68,13 @@ pnpm dev:api
pnpm dev:web
```
+> **Conflit de port possible sur `5432`** : si tu as déjà un Postgres natif installé
+> sur ta machine (service Windows, Homebrew, etc.), il peut occuper le port 5432 et
+> intercepter les connexions à la place du conteneur Docker (symptôme : Prisma
+> renvoie `P1000: Authentication failed` alors que les identifiants sont corrects).
+> Dans ce cas, mets `POSTGRES_PORT=5433` (ou autre) dans ton `.env` **et** adapte le
+> port dans la `DATABASE_URL` de `apps/api/.env`.
+
## Qualité / Tests
```bash
diff --git a/apps/api/prisma/migrations/20260816100611_init/migration.sql b/apps/api/prisma/migrations/20260816100611_init/migration.sql
new file mode 100644
index 0000000..2d429cd
--- /dev/null
+++ b/apps/api/prisma/migrations/20260816100611_init/migration.sql
@@ -0,0 +1,271 @@
+-- CreateTable
+CREATE TABLE "house" (
+ "id" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+
+ CONSTRAINT "house_pkey" PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "house" IS 'Foyer regroupant un ou plusieurs profils utilisateurs et leurs plannings.';
+COMMENT ON COLUMN "house"."id" IS 'Identifiant';
+COMMENT ON COLUMN "house"."name" IS 'Nom du foyer';
+
+-- CreateTable
+CREATE TABLE "diet" (
+ "id" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+
+ CONSTRAINT "diet_pkey" PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "diet" IS 'Régime alimentaire pouvant être suivi par un profil utilisateur.';
+COMMENT ON COLUMN "diet"."id" IS 'Identifiant';
+COMMENT ON COLUMN "diet"."name" IS 'Nom du régime alimentaire';
+
+-- CreateTable
+CREATE TABLE "category" (
+ "id" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+
+ CONSTRAINT "category_pkey" PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "category" IS 'Table d''énumération, destinée à grandir au fil du projet (portera notamment les nuances liées aux allergies).';
+COMMENT ON COLUMN "category"."id" IS 'Identifiant';
+COMMENT ON COLUMN "category"."name" IS 'Nom de la catégorie';
+
+-- CreateTable
+CREATE TABLE "allergy" (
+ "id" SERIAL NOT NULL,
+ "cat_id" INTEGER NOT NULL,
+
+ CONSTRAINT "allergy_pkey" PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "allergy" IS 'Allergie, rattachée à une catégorie. Associée à user_profiles en many-to-many (table de jointure simple, sans champ additionnel).';
+COMMENT ON COLUMN "allergy"."id" IS 'Identifiant';
+COMMENT ON COLUMN "allergy"."cat_id" IS 'FK → category';
+
+-- CreateTable
+CREATE TABLE "user_profiles" (
+ "id" SERIAL NOT NULL,
+ "first_name" TEXT NOT NULL,
+ "last_name" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "house_id" INTEGER,
+ "diet_id" INTEGER,
+
+ CONSTRAINT "user_profiles_pkey" PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "user_profiles" IS 'Profil utilisateur, rattaché à un foyer et éventuellement à un régime alimentaire.';
+COMMENT ON COLUMN "user_profiles"."id" IS 'Identifiant';
+COMMENT ON COLUMN "user_profiles"."first_name" IS 'Prénom';
+COMMENT ON COLUMN "user_profiles"."last_name" IS 'Nom';
+COMMENT ON COLUMN "user_profiles"."email" IS 'Email';
+COMMENT ON COLUMN "user_profiles"."house_id" IS 'FK → house';
+COMMENT ON COLUMN "user_profiles"."diet_id" IS 'FK → diet';
+
+-- CreateTable
+CREATE TABLE "user_profile_allergy" (
+ "user_profile_id" INTEGER NOT NULL,
+ "allergy_id" INTEGER NOT NULL,
+
+ CONSTRAINT "user_profile_allergy_pkey" PRIMARY KEY ("user_profile_id","allergy_id")
+);
+
+COMMENT ON TABLE "user_profile_allergy" IS 'Table de jointure simple pour l''association many-to-many user_profiles ↔ allergy (sans champ additionnel, non présente telle quelle dans la spec d''origine).';
+COMMENT ON COLUMN "user_profile_allergy"."user_profile_id" IS 'FK → user_profiles';
+COMMENT ON COLUMN "user_profile_allergy"."allergy_id" IS 'FK → allergy';
+
+-- CreateTable
+CREATE TABLE "planning" (
+ "id" SERIAL NOT NULL,
+ "start_date" DATE NOT NULL,
+ "finish_date" DATE NOT NULL,
+ "house_id" INTEGER NOT NULL,
+
+ CONSTRAINT "planning_pkey" PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "planning" IS 'Planning de repas d''un foyer sur une période donnée.';
+COMMENT ON COLUMN "planning"."id" IS 'Identifiant';
+COMMENT ON COLUMN "planning"."start_date" IS 'Date de début';
+COMMENT ON COLUMN "planning"."finish_date" IS 'Date de fin';
+COMMENT ON COLUMN "planning"."house_id" IS 'FK → house';
+
+-- CreateTable
+CREATE TABLE "planning_item" (
+ "id" SERIAL NOT NULL,
+ "planning_id" INTEGER NOT NULL,
+ "week_day" TEXT NOT NULL,
+ "meal" TEXT NOT NULL,
+ "recipe_id" INTEGER NOT NULL,
+
+ CONSTRAINT "planning_item_pkey" PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "planning_item" IS 'Entrée d''un planning : une recette assignée à un jour et un repas donnés.';
+COMMENT ON COLUMN "planning_item"."id" IS 'Identifiant';
+COMMENT ON COLUMN "planning_item"."planning_id" IS 'FK → planning';
+COMMENT ON COLUMN "planning_item"."week_day" IS 'Jour de la semaine';
+COMMENT ON COLUMN "planning_item"."meal" IS 'Repas concerné';
+COMMENT ON COLUMN "planning_item"."recipe_id" IS 'FK → recipe';
+
+-- CreateTable
+CREATE TABLE "sources" (
+ "id" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+ "url" TEXT,
+
+ CONSTRAINT "sources_pkey" PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "sources" IS 'Source d''origine d''une recette (site, livre, etc.), utilisée par le pipeline d''import.';
+COMMENT ON COLUMN "sources"."id" IS 'Identifiant';
+COMMENT ON COLUMN "sources"."name" IS 'Nom de la source';
+COMMENT ON COLUMN "sources"."url" IS 'URL';
+
+-- CreateTable
+CREATE TABLE "recipe" (
+ "id" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+ "source_id" INTEGER,
+ "description" TEXT,
+ "picture" TEXT,
+
+ CONSTRAINT "recipe_pkey" PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "recipe" IS 'Recette de cuisine. Associée à ingredients en many-to-many (voir recipe_ingredient).';
+COMMENT ON COLUMN "recipe"."id" IS 'Identifiant';
+COMMENT ON COLUMN "recipe"."name" IS 'Nom de la recette';
+COMMENT ON COLUMN "recipe"."source_id" IS 'FK → sources';
+COMMENT ON COLUMN "recipe"."description" IS 'Description';
+COMMENT ON COLUMN "recipe"."picture" IS 'Image';
+
+-- CreateTable
+CREATE TABLE "ingredients" (
+ "id" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+ "icon" TEXT,
+ "alternate_recipe" INTEGER,
+
+ CONSTRAINT "ingredients_pkey" PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "ingredients" IS 'Ingrédient pouvant entrer dans la composition d''une ou plusieurs recettes.';
+COMMENT ON COLUMN "ingredients"."id" IS 'Identifiant';
+COMMENT ON COLUMN "ingredients"."name" IS 'Nom';
+COMMENT ON COLUMN "ingredients"."icon" IS 'Icône';
+COMMENT ON COLUMN "ingredients"."alternate_recipe" IS 'FK → recipe (recette alternative, ex: faire soi-même plutôt qu''acheter)';
+
+-- CreateTable
+CREATE TABLE "recipe_ingredient" (
+ "recipe_id" INTEGER NOT NULL,
+ "ingredient_id" INTEGER NOT NULL,
+ "quantity" DECIMAL(10,2) NOT NULL,
+ "unit" TEXT NOT NULL,
+
+ CONSTRAINT "recipe_ingredient_pkey" PRIMARY KEY ("recipe_id","ingredient_id")
+);
+
+COMMENT ON TABLE "recipe_ingredient" IS 'Association recipe ↔ ingredients enrichie d''une quantité et d''une unité (non présent dans la spec d''origine, nécessaire pour les listes de courses / le calcul batch-cooking).';
+COMMENT ON COLUMN "recipe_ingredient"."recipe_id" IS 'FK → recipe';
+COMMENT ON COLUMN "recipe_ingredient"."ingredient_id" IS 'FK → ingredients';
+COMMENT ON COLUMN "recipe_ingredient"."quantity" IS 'Quantité de l''ingrédient nécessaire pour la recette';
+COMMENT ON COLUMN "recipe_ingredient"."unit" IS 'Unité de mesure de la quantité (g, ml, pièce...)';
+
+-- CreateTable
+CREATE TABLE "tech_step" (
+ "id" SERIAL NOT NULL,
+
+ CONSTRAINT "tech_step_pkey" PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "tech_step" IS 'Technique culinaire réutilisable (ex: éplucher, mixer, cuire...), référencée par les étapes de recette.';
+COMMENT ON COLUMN "tech_step"."id" IS 'Identifiant';
+
+-- CreateTable
+CREATE TABLE "tech_step_mapping" (
+ "id" SERIAL NOT NULL,
+ "tech_step_id" INTEGER NOT NULL,
+ "expression" TEXT NOT NULL,
+ "weight" INTEGER NOT NULL,
+
+ CONSTRAINT "tech_step_mapping_pkey" PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "tech_step_mapping" IS 'Utilisé par le pipeline d''import de recette pour détecter automatiquement la technique correspondant à une instruction brute (expression = motif texte, weight = score de correspondance).';
+COMMENT ON COLUMN "tech_step_mapping"."id" IS 'Identifiant';
+COMMENT ON COLUMN "tech_step_mapping"."tech_step_id" IS 'FK → tech_step';
+COMMENT ON COLUMN "tech_step_mapping"."expression" IS 'Expression';
+COMMENT ON COLUMN "tech_step_mapping"."weight" IS 'Poids';
+
+-- CreateTable
+CREATE TABLE "step" (
+ "id" SERIAL NOT NULL,
+ "recipe_id" INTEGER NOT NULL,
+ "description" TEXT NOT NULL,
+ "picture" TEXT,
+ "order" INTEGER NOT NULL,
+ "tech_step_id" INTEGER,
+
+ CONSTRAINT "step_pkey" PRIMARY KEY ("id")
+);
+
+COMMENT ON TABLE "step" IS 'Étape d''une recette. Appartient à exactement une recette (one-to-many depuis recipe) : le champ order n''a de sens que dans le contexte d''une recette donnée — voir specs/batch-cooking-modele.md pour la formulation d''origine (many-to-many).';
+COMMENT ON COLUMN "step"."id" IS 'Identifiant';
+COMMENT ON COLUMN "step"."recipe_id" IS 'FK → recipe';
+COMMENT ON COLUMN "step"."description" IS 'Description de l''étape';
+COMMENT ON COLUMN "step"."picture" IS 'Image';
+COMMENT ON COLUMN "step"."order" IS 'Ordre dans la recette';
+COMMENT ON COLUMN "step"."tech_step_id" IS 'FK → tech_step';
+
+-- CreateIndex
+CREATE UNIQUE INDEX "user_profiles_email_key" ON "user_profiles"("email");
+
+-- AddForeignKey
+ALTER TABLE "allergy" ADD CONSTRAINT "allergy_cat_id_fkey" FOREIGN KEY ("cat_id") REFERENCES "category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "user_profiles" ADD CONSTRAINT "user_profiles_house_id_fkey" FOREIGN KEY ("house_id") REFERENCES "house"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "user_profiles" ADD CONSTRAINT "user_profiles_diet_id_fkey" FOREIGN KEY ("diet_id") REFERENCES "diet"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "user_profile_allergy" ADD CONSTRAINT "user_profile_allergy_user_profile_id_fkey" FOREIGN KEY ("user_profile_id") REFERENCES "user_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "user_profile_allergy" ADD CONSTRAINT "user_profile_allergy_allergy_id_fkey" FOREIGN KEY ("allergy_id") REFERENCES "allergy"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "planning" ADD CONSTRAINT "planning_house_id_fkey" FOREIGN KEY ("house_id") REFERENCES "house"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "planning_item" ADD CONSTRAINT "planning_item_planning_id_fkey" FOREIGN KEY ("planning_id") REFERENCES "planning"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "planning_item" ADD CONSTRAINT "planning_item_recipe_id_fkey" FOREIGN KEY ("recipe_id") REFERENCES "recipe"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "recipe" ADD CONSTRAINT "recipe_source_id_fkey" FOREIGN KEY ("source_id") REFERENCES "sources"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ingredients" ADD CONSTRAINT "ingredients_alternate_recipe_fkey" FOREIGN KEY ("alternate_recipe") REFERENCES "recipe"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "recipe_ingredient" ADD CONSTRAINT "recipe_ingredient_recipe_id_fkey" FOREIGN KEY ("recipe_id") REFERENCES "recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "recipe_ingredient" ADD CONSTRAINT "recipe_ingredient_ingredient_id_fkey" FOREIGN KEY ("ingredient_id") REFERENCES "ingredients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "tech_step_mapping" ADD CONSTRAINT "tech_step_mapping_tech_step_id_fkey" FOREIGN KEY ("tech_step_id") REFERENCES "tech_step"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "step" ADD CONSTRAINT "step_recipe_id_fkey" FOREIGN KEY ("recipe_id") REFERENCES "recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "step" ADD CONSTRAINT "step_tech_step_id_fkey" FOREIGN KEY ("tech_step_id") REFERENCES "tech_step"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/apps/api/prisma/migrations/migration_lock.toml b/apps/api/prisma/migrations/migration_lock.toml
new file mode 100644
index 0000000..fbffa92
--- /dev/null
+++ b/apps/api/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "postgresql"
\ No newline at end of file
diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma
index 4856958..ba92c96 100644
--- a/apps/api/prisma/schema.prisma
+++ b/apps/api/prisma/schema.prisma
@@ -7,4 +7,203 @@ datasource db {
url = env("DATABASE_URL")
}
-// Models will be added once the data model is specified.
+// -----------------------------------------------------------------------------
+// Users & household
+// See specs/batch-cooking-modele.md for the source data model documentation.
+// -----------------------------------------------------------------------------
+
+model House {
+ id Int @id @default(autoincrement())
+ name String
+
+ members UserProfile[]
+ plannings Planning[]
+
+ @@map("house")
+}
+
+model Diet {
+ id Int @id @default(autoincrement())
+ name String
+
+ users UserProfile[]
+
+ @@map("diet")
+}
+
+/// Enumeration-style table, meant to grow over time (e.g. allergy nuances).
+model Category {
+ id Int @id @default(autoincrement())
+ name String
+
+ allergies Allergy[]
+
+ @@map("category")
+}
+
+model Allergy {
+ id Int @id @default(autoincrement())
+ categoryId Int @map("cat_id")
+
+ category Category @relation(fields: [categoryId], references: [id])
+ users UserProfileAllergy[]
+
+ @@map("allergy")
+}
+
+model UserProfile {
+ id Int @id @default(autoincrement())
+ firstName String @map("first_name")
+ lastName String @map("last_name")
+ email String @unique
+ houseId Int? @map("house_id")
+ dietId Int? @map("diet_id")
+
+ house House? @relation(fields: [houseId], references: [id], onDelete: SetNull)
+ diet Diet? @relation(fields: [dietId], references: [id], onDelete: SetNull)
+ allergies UserProfileAllergy[]
+
+ @@map("user_profiles")
+}
+
+/// Explicit join table for the user_profiles <-> allergy association
+/// (documented in the spec as a plain many-to-many, no extra fields).
+model UserProfileAllergy {
+ userProfileId Int @map("user_profile_id")
+ allergyId Int @map("allergy_id")
+
+ userProfile UserProfile @relation(fields: [userProfileId], references: [id], onDelete: Cascade)
+ allergy Allergy @relation(fields: [allergyId], references: [id], onDelete: Cascade)
+
+ @@id([userProfileId, allergyId])
+ @@map("user_profile_allergy")
+}
+
+// -----------------------------------------------------------------------------
+// Planning
+// -----------------------------------------------------------------------------
+
+model Planning {
+ id Int @id @default(autoincrement())
+ startDate DateTime @map("start_date") @db.Date
+ finishDate DateTime @map("finish_date") @db.Date
+ houseId Int @map("house_id")
+
+ house House @relation(fields: [houseId], references: [id], onDelete: Cascade)
+ items PlanningItem[]
+
+ @@map("planning")
+}
+
+model PlanningItem {
+ id Int @id @default(autoincrement())
+ planningId Int @map("planning_id")
+ weekDay String @map("week_day")
+ meal String
+ recipeId Int @map("recipe_id")
+
+ planning Planning @relation(fields: [planningId], references: [id], onDelete: Cascade)
+ recipe Recipe @relation(fields: [recipeId], references: [id])
+
+ @@map("planning_item")
+}
+
+// -----------------------------------------------------------------------------
+// Recipes
+// -----------------------------------------------------------------------------
+
+model Source {
+ id Int @id @default(autoincrement())
+ name String
+ url String?
+
+ recipes Recipe[]
+
+ @@map("sources")
+}
+
+model Recipe {
+ id Int @id @default(autoincrement())
+ name String
+ sourceId Int? @map("source_id")
+ description String?
+ picture String?
+
+ source Source? @relation(fields: [sourceId], references: [id], onDelete: SetNull)
+ ingredients RecipeIngredient[]
+ steps Step[]
+ planningItems PlanningItem[]
+ /// Ingredients for which this recipe is offered as a make-it-yourself alternative.
+ alternateFor Ingredient[] @relation("IngredientAlternateRecipe")
+
+ @@map("recipe")
+}
+
+model Ingredient {
+ id Int @id @default(autoincrement())
+ name String
+ icon String?
+ alternateRecipeId Int? @map("alternate_recipe")
+
+ alternateRecipe Recipe? @relation("IngredientAlternateRecipe", fields: [alternateRecipeId], references: [id], onDelete: SetNull)
+ recipes RecipeIngredient[]
+
+ @@map("ingredients")
+}
+
+/// recipe <-> ingredients association. The spec documents this as a plain
+/// many-to-many, but a shopping list / batch-cooking calculation needs a
+/// quantity per recipe, so this join table carries quantity + unit
+/// (project decision, not in the original spec doc).
+model RecipeIngredient {
+ recipeId Int @map("recipe_id")
+ ingredientId Int @map("ingredient_id")
+ quantity Decimal @db.Decimal(10, 2)
+ unit String
+
+ recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
+ ingredient Ingredient @relation(fields: [ingredientId], references: [id], onDelete: Cascade)
+
+ @@id([recipeId, ingredientId])
+ @@map("recipe_ingredient")
+}
+
+model TechStep {
+ id Int @id @default(autoincrement())
+
+ steps Step[]
+ mappings TechStepMapping[]
+
+ @@map("tech_step")
+}
+
+/// Used by the recipe-import pipeline to auto-detect which technique a raw
+/// instruction step corresponds to (expression = text pattern, weight = match score).
+model TechStepMapping {
+ id Int @id @default(autoincrement())
+ techStepId Int @map("tech_step_id")
+ expression String
+ weight Int
+
+ techStep TechStep @relation(fields: [techStepId], references: [id], onDelete: Cascade)
+
+ @@map("tech_step_mapping")
+}
+
+/// Modeled as one-to-many (a step belongs to exactly one recipe), not the
+/// many-to-many noted in the spec doc: `order` only makes sense scoped to a
+/// single recipe, which isn't reconcilable with steps being shared across
+/// recipes. See specs/batch-cooking-modele.md for the original wording.
+model Step {
+ id Int @id @default(autoincrement())
+ recipeId Int @map("recipe_id")
+ description String
+ picture String?
+ order Int
+ techStepId Int? @map("tech_step_id")
+
+ recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
+ techStep TechStep? @relation(fields: [techStepId], references: [id], onDelete: SetNull)
+
+ @@map("step")
+}
diff --git a/specs/batch-cooking-architecture.md b/specs/batch-cooking-architecture.md
new file mode 100644
index 0000000..9332a8a
--- /dev/null
+++ b/specs/batch-cooking-architecture.md
@@ -0,0 +1,72 @@
+# Architecture technique — Projet Batch-cooking
+
+> Documentation de l'architecture serveur/client de l'application.
+
+---
+
+## Vue d'ensemble
+
+L'application repose sur une architecture **client-serveur** classique :
+
+- Un **serveur** exposant une **API** (échanges standards) et un canal **websocket** (communication temps réel)
+- Plusieurs **clients** (Client 1, Client 2, Client 3...) connectés simultanément au serveur
+- Une base de données **PostgreSQL**
+
+```mermaid
+flowchart TB
+ subgraph SERVER["Server"]
+ WS["Web socket"]
+ API["API"]
+ CALC["Calcul batch-cooking
(TODO)"]
+ IMPORT["Import d'une recette"]
+ IMP1["Import depuis source"]
+ IMP2["Traduction en étapes"]
+ IMP3["Sauvegarde"]
+ DB[("Database
PostgreSQL")]
+
+ IMPORT --> IMP1 --> IMP2 --> IMP3 --> DB
+ CALC --> WS
+ end
+
+ C1["Client 1"]
+ C2["Client 2"]
+ C3["Client 3"]
+
+ API <--> C1
+ API <--> C2
+ API <--> C3
+ WS --> C1
+ WS --> C2
+ WS --> C3
+
+ style SERVER fill:none,stroke:#888,stroke-width:1px
+```
+
+---
+
+## Composants
+
+### API
+Point d'entrée principal pour les échanges entre les clients et le serveur (requêtes classiques).
+
+### Web socket
+Canal de communication temps réel entre le serveur et les clients connectés.
+
+### Module « Calcul batch-cooking »
+Logique de calcul du batch-cooking (optimisation du planning/des recettes selon le planning). **Statut : TODO — reste à développer.**
+
+### Module « Import d'une recette »
+Pipeline d'ajout d'une recette, en trois étapes :
+1. **Import depuis source** — récupération de la recette (via `sources`)
+2. **Traduction en étapes** — découpage en `step` / `tech_step`
+3. **Sauvegarde** — persistance en base de données
+
+### Database (PostgreSQL)
+Stockage de l'ensemble des données de l'application (voir le modèle de données pour le détail des tables).
+
+---
+
+## Notes
+
+- Le module de calcul batch-cooking est le principal chantier restant côté serveur (TODO).
+- Le websocket est utilisé pour la communication temps réel, en complément de l'API.
diff --git a/specs/batch-cooking-modele.md b/specs/batch-cooking-modele.md
new file mode 100644
index 0000000..32804f2
--- /dev/null
+++ b/specs/batch-cooking-modele.md
@@ -0,0 +1,251 @@
+# Modèle de données — Projet Batch-cooking
+
+> Documentation du schéma de données de l'application de planification de batch-cooking.
+
+---
+
+## Vue d'ensemble
+
+Le modèle s'articule autour de trois grands pôles :
+
+- **Utilisateurs & foyer** — `user_profiles`, `house`, `diet`, `allergy`, `category`
+- **Planification** — `planning`, `planning_item`
+- **Recettes** — `recipe`, `ingredients`, `step`, `tech_step`, `tech_step_mapping`, `sources`
+
+---
+
+## Schéma entité-relation
+
+```mermaid
+erDiagram
+ USER_PROFILES }o--|| HOUSE : "vit dans"
+ USER_PROFILES }o--|| DIET : "suit"
+ HOUSE ||--o{ PLANNING : "planifie"
+ PLANNING ||--o{ PLANNING_ITEM : "contient"
+ PLANNING_ITEM }o--|| RECIPE : "utilise"
+ RECIPE }o--|| SOURCES : "vient de"
+ CATEGORY ||--o{ ALLERGY : "classe"
+ USER_PROFILES }o--o{ ALLERGY : "a"
+ RECIPE }o--o{ INGREDIENTS : "compose de"
+ RECIPE }o--o{ STEP : "compose de"
+ STEP }o--|| TECH_STEP : "utilise"
+ TECH_STEP ||--o{ TECH_STEP_MAPPING : "mappe"
+ INGREDIENTS }o--|| RECIPE : "recette alternative"
+
+ USER_PROFILES {
+ int id PK
+ string first_name
+ string last_name
+ string email
+ int house_id FK
+ int diet_id FK
+ }
+ HOUSE {
+ int id PK
+ string name
+ }
+ PLANNING {
+ int id PK
+ date start_date
+ date finish_date
+ int house_id FK
+ }
+ PLANNING_ITEM {
+ int id PK
+ int planning_id FK
+ string week_day
+ string meal
+ int recipe_id FK
+ }
+ DIET {
+ int id PK
+ string name
+ }
+ ALLERGY {
+ int id PK
+ int cat_id FK
+ }
+ CATEGORY {
+ int id PK
+ string name
+ }
+ INGREDIENTS {
+ int id PK
+ string name
+ string icon
+ int alternate_recipe FK
+ }
+ RECIPE {
+ int id PK
+ string name
+ int source_id FK
+ string description
+ string picture
+ }
+ STEP {
+ int id PK
+ string description
+ string picture
+ int order
+ int tech_step_id FK
+ }
+ TECH_STEP {
+ int id PK
+ }
+ TECH_STEP_MAPPING {
+ int id PK
+ int tech_step_id FK
+ string expression
+ int weight
+ }
+ SOURCES {
+ int id PK
+ string name
+ string url
+ }
+```
+
+*(Rendu sur les visualiseurs markdown compatibles mermaid — GitHub, VS Code, Obsidian, etc.)*
+
+---
+
+## Tables
+
+### `user_profiles`
+| Champ | Description |
+|---|---|
+| `id` | Identifiant |
+| `first_name` | Prénom |
+| `last_name` | Nom |
+| `email` | Email |
+| `house_id` | FK → `house` |
+| `diet_id` | FK → `diet` |
+
+### `house`
+| Champ | Description |
+|---|---|
+| `id` | Identifiant |
+| `name` | Nom du foyer |
+
+### `diet`
+| Champ | Description |
+|---|---|
+| `id` | Identifiant |
+| `name` | Nom du régime alimentaire |
+
+### `allergy`
+| Champ | Description |
+|---|---|
+| `id` | Identifiant |
+| `cat_id` | FK → `category` |
+
+Associée à `user_profiles` en many-to-many (table de jointure simple, sans champ additionnel).
+
+### `category`
+| Champ | Description |
+|---|---|
+| `id` | Identifiant |
+| `name` | Nom de la catégorie |
+
+Table d'énumération, destinée à grandir au fil du projet (portera notamment les nuances liées aux allergies).
+
+### `planning`
+| Champ | Description |
+|---|---|
+| `id` | Identifiant |
+| `start_date` | Date de début |
+| `finish_date` | Date de fin |
+| `house_id` | FK → `house` |
+
+### `planning_item`
+| Champ | Description |
+|---|---|
+| `id` | Identifiant |
+| `planning_id` | FK → `planning` |
+| `week_day` | Jour de la semaine |
+| `meal` | Repas concerné |
+| `recipe_id` | FK → `recipe` |
+
+### `recipe`
+| Champ | Description |
+|---|---|
+| `id` | Identifiant |
+| `name` | Nom de la recette |
+| `source_id` | FK → `sources` |
+| `description` | Description |
+| `picture` | Image |
+
+Associée à `ingredients` en many-to-many.
+
+### `ingredients`
+| Champ | Description |
+|---|---|
+| `id` | Identifiant |
+| `name` | Nom |
+| `icon` | Icône |
+| `alternate_recipe` | FK → `recipe` (recette alternative) |
+
+### `step`
+| Champ | Description |
+|---|---|
+| `id` | Identifiant |
+| `description` | Description de l'étape |
+| `picture` | Image |
+| `order` | Ordre dans la recette |
+| `tech_step_id` | FK → `tech_step` |
+
+Associée à `recipe` en many-to-many.
+
+### `tech_step`
+| Champ | Description |
+|---|---|
+| `id` | Identifiant |
+
+### `tech_step_mapping`
+| Champ | Description |
+|---|---|
+| `id` | Identifiant |
+| `tech_step_id` | FK → `tech_step` |
+| `expression` | Expression |
+| `weight` | Poids |
+
+### `sources`
+| Champ | Description |
+|---|---|
+| `id` | Identifiant |
+| `name` | Nom de la source |
+| `url` | URL |
+
+---
+
+## Relations
+
+### Many-to-one (clés étrangères)
+
+| Table source | Champ FK | Table cible |
+|---|---|---|
+| `user_profiles` | `house_id` | `house` |
+| `user_profiles` | `diet_id` | `diet` |
+| `planning` | `house_id` | `house` |
+| `planning_item` | `planning_id` | `planning` |
+| `planning_item` | `recipe_id` | `recipe` |
+| `allergy` | `cat_id` | `category` |
+| `ingredients` | `alternate_recipe` | `recipe` |
+| `recipe` | `source_id` | `sources` |
+| `step` | `tech_step_id` | `tech_step` |
+| `tech_step_mapping` | `tech_step_id` | `tech_step` |
+
+### Many-to-many (associations)
+
+| Table A | Table B | Détail |
+|---|---|---|
+| `user_profiles` | `allergy` | Table de jointure simple |
+| `recipe` | `ingredients` | Composition d'une recette |
+| `step` | `recipe` | Étapes d'une recette |
+
+---
+
+## Règles de modélisation
+
+- Toute relation qualifiée d'**« association »** entre deux tables est une relation **many-to-many**.
+- `category` est une table d'énumération, amenée à grandir au fur et à mesure du projet.