Preview
AutoMeal
AutoMeal is a nutrition app for people who cook at home: set a goal, stock a pantry, cook from what you have, and shop the rest. The rule in the codebase is strict. Nutrition numbers come from the food database. A model may name a food. It does not invent a calorie.
The problem
A meal plan that ignores the cupboard creates a shopping list you did not need, and a number you cannot trust. AutoMeal separates those jobs. Targets are calculated. Foods are matched. Macros are summed from ingredient rows. The model is only allowed to talk about numbers a tool just returned.
What the preview does
- Onboarding turns age, sex, height, weight, activity and goal into daily energy and macros with Mifflin-St Jeor and explicit safety floors. Deficits are blocked for pregnancy, breastfeeding, a BMI under 18.5, and under-18s. Protein reference weight is capped when BMI is over 30.
- The pantry accepts typed items and a photo. Detected labels go to a review board. Nothing is trusted until the person confirms it. Photo metadata, including location, is stripped on upload. HEIC is re-encoded to JPEG and the original bytes are not kept.
- Recipes are ranked by fewest missing ingredients, then by the remaining macro budget, equipment and diet. Allergens use safety-first rules: a missing row means unknown, never free-from. Severe allergies hide contain, may-contain and unknown. "Avoid" hides only contain, and warns on the rest.
- A barcode camera looks up products already imported from Open Food Facts. It does not call that service live. The shopping list is de-duplicated and grouped by aisle, and it can deep-link to a search on Tesco, Sainsbury's, Asda, Waitrose, Ocado, Morrisons or Amazon Fresh through an allowlisted redirect. There is no checkout.
- An Ask sheet is a chat over the same tools. A recipe importer accepts a public TikTok or Instagram URL, fetches the caption only, and asks a model for an ingredient draft. Macros are then computed from the database. The recipe is saved as private.
Architecture
It is a pnpm and Turborepo monorepo, with uv for Python. apps/web is Next.js 16. apps/api is FastAPI. The browser never holds the preview token: Next proxies /api and adds it on the server. A middleware gates the private preview, sends noindex, and turns the admin into a 404 for anyone who is not an admin. TypeScript types for the web app are generated from the API's OpenAPI schema.
The model is not the calculator
The agent is PydanticAI. The default model is gpt-4o-mini. If the model is set to test, or there is no API key, a deterministic offline model answers instead. The private preview and continuous integration use that offline model. The recipe importer calls Chat Completions at temperature 0, with a cap of 10 imports a day and 8,000 characters of caption, and refuses its mock provider outside local and test. If there is no key, import is unavailable.
Photo scan is a real pipeline with a pretend eye. The vision interface may return ingredient labels only. A schema rejects any nutrient fields. The only working provider maps fixture images to a fixed label list. An OpenAI vision provider exists as a stub and does not build an HTTP client, so no photo leaves the process. Matching a label to a food is retrieval inside Postgres: exact alias, exact class name, a trigram fuzzy match, then cosine distance on a 384-dimension vector. That vector is a hashed bag of tokens, not a neural embedding. The seed holds 1,190 aliases and 353 ingredient classes. The match step never reads nutrient columns.
Before any model call, a rule router sends emergencies, self-harm, under-18s, pregnancy, supplements, medical questions and eating-disorder language to fixed replies. The first matching category wins. Tools take the user id from the run context, never from the model's arguments. Read tools search foods, compute nutrition, list the pantry and suggest recipes, with allergen and diet filters applied in SQL. Write tools only propose a change. Nothing is saved until the person confirms, and a goal update that breaks the safety floor is rejected. After the draft reply, every number with a unit must appear in a ledger of values the tools actually returned. A canary string catches a leaked system prompt. Failure asks the model to retry, then falls back to a safe reply. External text, including captions, is wrapped as untrusted data. Every run and tool call is stored.
Asking what to cook
Data and the numbers
Postgres 17 with pgvector and pg_trgm holds two schemas, food and app, across 22 Alembic revisions. The committed seed is a curated subset: 354 foods (307 from CoFID, 6 USDA Foundation, 41 USDA SR Legacy), 4,491 nutrient rows, 528 portions, 4,956 allergen rows with no unknown cells, 2,023 diet-tag rows and 26 recipes. Open Food Facts is never stored as free-from. Allergen basis distinguishes a source label, an intrinsic generic, typical cross-contact and a typical UK recipe. Energy targets use an explicit floor, and the maximum deficit is the smallest of 20 percent of TDEE, 500 kcal, and 1 percent of body weight per week.
Forty-three safety and behaviour cases, covering numbers, allergens, label checks, goals, floors, eating disorders, minors, medicine, pregnancy, injection and tone, run against the mock model as the CI gate. The same cases can be pointed at the live model when a key is present. That run is opt-in.
Security, delivery and tests
Sign-in is a six-digit email code through Supabase Auth, used only to send and check the code. The app session is AutoMeal's own cookie, and tokens are stored as SHA-256 hashes. The API refuses to start if magic-link exposure is turned on against a non-local database. Admin rights are granted only after the email is verified, and a non-admin receives a 404 rather than a 403. The kitchen screens in the preview still identify a single demo user with a header that is allowed only in local and test. Health notes can be encrypted at the application layer, and production will not boot without that key. Waitlist IP addresses are stored as peppered hashes. The importer fetches only allowlisted hosts, checks the address is public, and caps the response at 1.5 MB.
The preview is free tier: the web app and landing on Vercel, the API as one Docker service on Render, Postgres on Supabase. The API binds its port first, then migrates and seeds in the background, and the web app shows a warming-up screen until health says ready. Continuous integration runs ruff, mypy, pytest, Bandit and pip-audit for the API, then lint, typecheck, Vitest and 15 Playwright specs against the offline model and the mock vision provider. A separate job runs gitleaks, Semgrep, Bandit and dependency audits. There is no error-tracking service in the repository. /health reports whether migrations and the database are ready, and the agent audit tables are the trail.
Where it is going
This is a feature-built preview, not an open product. The screens and the agent are real. Photo recognition is not, and the preview does not call the live model. The code points next at real account sessions, a vision provider behind the interface that already rejects invented nutrients, fuller CoFID, USDA and Open Food Facts loads, and the Spanish locale once the food-data licence is settled. Supermarket ordering is not in the plan that ships. Search links are the integration.