- Python 51%
- TypeScript 47.3%
- Shell 0.8%
- Makefile 0.3%
- CSS 0.3%
- Other 0.2%
| .forgejo/workflows | ||
| apps | ||
| data | ||
| docs | ||
| packages | ||
| scripts | ||
| .coverage-threshold | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .pre-commit-config.yaml | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| CODEX_SCAFFOLDING_PROMPT.md | ||
| compose.yaml | ||
| IDEAS.md | ||
| Makefile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
Composable Cooking
Technique × Flavor Profile × Ingredients × Form Factor × Finishing → Meal
A mobile-first web application that composes meals from reusable culinary patterns instead of searching a recipe database. Tell it what is in your kitchen and how long you have; it returns ranked, explained candidates and resolves one into exact quantities, ordered instructions and macros.
Composition, constraints and nutrition are deterministic. No LLM is involved in the core loop, and none is required to run the application.
Prerequisites
- Docker and Docker Compose — the only requirement to run the application
- For development: uv, Python 3.14, Node.js 22
First start
cp .env.example .env
docker compose up --build
The API container migrates the database and loads the curated knowledge base before it starts serving, so a clean checkout comes up working.
| Web app | http://localhost:3000 |
| API docs (Swagger) | http://localhost:8000/docs |
| OpenAPI document | http://localhost:8000/openapi.json |
| Health | http://localhost:8000/health |
make up and make down wrap the same thing.
Architecture
apps/web (Next.js) ──REST/OpenAPI──▶ apps/api (FastAPI) ──▶ PostgreSQL
│
CompositionEngine
(pure, deterministic)
The API is independent of the web app, so a native iOS client can be added later against the same contract.
| Path | Contents |
|---|---|
apps/api/ |
FastAPI backend, domain model, composition and nutrition engines, persistence, migrations, seed loader, tests |
apps/web/ |
Next.js mobile-first frontend, component tests, Playwright suite |
packages/ontology/ |
LinkML schema — the authoritative vocabulary |
packages/api-client/ |
TypeScript client generated from the OpenAPI contract |
data/ |
Curated cooking knowledge as reviewable YAML |
docs/ |
Design, user stories, implementation plan, ADRs |
scripts/ |
Validation and generation helpers |
Read docs/DESIGN.md, docs/USER_STORIES.md and docs/adr/ before making
architectural changes. AGENTS.md is the working agreement for this repository.
How composition works
MealPattern and Meal are deliberately distinct. A pattern is an abstract
blueprint with no quantities; a meal is a concrete materialization with exact
grams, instructions, servings, nutrition and provenance back to its pattern.
The engine identifies eligible patterns, resolves a primary protein, checks technique and flavor compatibility, satisfies the form factor's required component roles, adds finishing components whose ingredients are available, calculates nutrition, applies hard constraints, and scores the survivors with explicit weights. Every step is deterministic: identical requests against identical data produce identical candidates in identical order.
Ranking weights live in exactly one place —
apps/api/src/composable_cooking/composition/weights.py.
Candidates are not stored. A candidate ID is a content hash of the composition, and materialization replays the request to recompute it (ADR-006).
Development
make install # backend and frontend dependencies
make install-hooks # the local quality gate
Run the stack outside Docker, with PostgreSQL still in Compose:
docker compose up -d db
make migrate
make seed
make backfill-nutrition # idempotent update for saved meals from older versions
cd apps/api && .venv/bin/uvicorn composable_cooking.main:app --reload
npm run dev --workspace apps/web
To run the API with no database at all — useful for frontend work — set
COMPOSABLE_COOKING_LOAD_CATALOG_FROM_DATABASE=false and it reads the curated
YAML directly.
Configuration is environment-driven and documented in .env.example. Never
commit a real .env.
Seed data
The curated knowledge base lives in data/ as YAML and is reviewed in Git. It
is deliberately small but recombinant: blackening appears as a bowl, a wrap and
a salad, the post-cook marinade appears as a salad and a pasta salad, and every
pattern accepts any of the four proteins.
IDs are stable and prefixed: ingredient:chicken-breast,
technique:blackening, flavor:cajun, form:bowl,
pattern:blackened-protein-bowl.
Ingredients marked pantry: true — oil, garlic, spices, lemon — are always
assumed available and never count toward ingredient coverage.
make ontology # validate the LinkML schema, seed files and cross-references
make seed # load into PostgreSQL (idempotent)
make ontology checks two different things: LinkML validates each file's
shape, and a Python validator checks what LinkML cannot — that every reference
resolves across files, that each pattern can satisfy its form factor, and that
declared roles agree with the ingredients they point at.
API client
FastAPI's OpenAPI document is authoritative for the HTTP contract. The frontend imports generated types and never declares an API shape by hand.
make api-client # regenerate after any request or response schema change
Commit packages/api-client/openapi.json and
packages/api-client/src/schema.ts. CI regenerates and fails if they are stale.
Tests
make test # backend pytest + frontend vitest
make coverage # backend suite with the coverage ratchet
make e2e # Playwright primary journey (needs a migrated database)
make check # the full gate — run this before handing work off
make check runs pre-commit, lint, type checks, ontology validation, both test
suites, coverage and the frontend build. CI runs the same command.
The end-to-end suite needs the database up and migrated:
docker compose up -d db && make migrate && make seed
make e2e-install # once, to fetch the browser
make e2e
The backend coverage ratchet in scripts/check_api_coverage.sh blocks any drop
below the high-water mark in .coverage-threshold and requires at least 80%
coverage for every measured backend file. Add tests rather than lowering it.
Deploying on the internet
The app is safe to expose publicly, behind your own reverse proxy — no account system is required to run it (see ADR-011 for the optional accounts layer), and hardening for internet-facing traffic is built in rather than configured on top. HTTPS is the only supported public deployment path; there is no guidance here for exposing this over plain HTTP.
Set these environment variables for the API when deploying publicly
(apps/api/src/composable_cooking/config.py is authoritative):
| Variable | Set to | Why |
|---|---|---|
COMPOSABLE_COOKING_CORS_ALLOW_ORIGINS |
Your real public origin(s), e.g. ["https://cooking.example.com"] |
Defaults to the localhost dev origin, which no public browser can use. |
COMPOSABLE_COOKING_DEVICE_COOKIE_SECURE |
true |
Marks the anonymous device-token cookie Secure; only meaningful once the app is actually served over HTTPS. |
COMPOSABLE_COOKING_SESSION_SECRET_KEY |
A real random secret | Signs the account session cookie (ADR-011). Defaults to a fixed dev value — anyone deploying accounts publicly must override this. |
COMPOSABLE_COOKING_OPENROUTER_API_KEY |
Your real OpenRouter key | Enables AI recipe images (US-041). Unset disables the feature with a clear error, not a crash. Never commit a real key. |
Everything else is already active with no configuration:
- Rate limiting on write endpoints and composition requests
(
apps/api/src/composable_cooking/api/rate_limit.py) — an unauthenticated device token is a bearer-style credential, so abuse protection applies regardless of accounts. - Security response headers —
X-Content-Type-Options,X-Frame-Options,Referrer-Policy, andStrict-Transport-Security(sent only when your reverse proxy forwardsX-Forwarded-Proto: https, since HSTS must never be sent over a connection that isn't actually TLS). If your reverse proxy also sets its own copies of these headers, check for duplicates rather than letting both write conflicting values.
None of this changes behavior for a trusted local-network deployment — a LAN install with the default settings works exactly as it did before.
License
MIT.