Case study

King of Meal Prep

A single-household kitchen platform where every change to stock is a reversible transaction. It runs at home in a hardened container, keeps its data in SQLite, and needs no cloud account to work.

Constraint

One household, one box, no account anywhere

Shape

The brief was a private system for one household, not a product for many. That removed multi-tenancy and sign-up flows from the design, and put the weight where the difficulty actually is: keeping a food inventory honest while someone cooks from it, changes their mind, and undoes things days later.

It serves a browser or an installed PWA behind a private reverse proxy. One Gunicorn process with eight threads runs Flask, a read-write SQLite application database, and a read-only SQLite nutrition index. An external model API and SMTP are optional and off by default, so the system keeps working when neither is reachable.

The reference container runs as an unprivileged user, drops all capabilities, sets no-new-privileges, and mounts a read-only root filesystem: only the runtime directory and a temporary filesystem are writable. That decision comes back further down this page.

Python, Flask, SQLite, Docker, PWA

Design

Inventory is the hard part, so it is written as contracts

Changing one of these rules requires tests and a migration note.

  • A recipe yield is the yield of one ingredient list. A recipe yielding four portions consumes its list once when four portions are prepared, and half of it for two. Portions eaten stay a separate number from portions produced.
  • Cooking is idempotent. Every cook transition carries an idempotency key, and repeating the same key returns the existing event instead of deducting stock twice. The guided step-by-step mode is not a second code path: it ends in the same transition.
  • Undo compensates, it does not delete. It reverses recorded movements while preserving unrelated later edits, and refuses when a later use depends on it until that use is undone first.
  • Prepared portions are consumed oldest-expiry first, and eating prepared food never touches raw pantry stock.
  • Pantry identity is an exact canonical key, with arithmetic in grams, millilitres or pieces, while the original display unit survives for the interface.
  • A split portion is derived, never duplicated. A 400 g row with a 200 g planned portion stays one row: a recipe taking 100 g leaves 300 g, shown as one and a half portions, and undoing that recipe restores both numbers.

Corrections

Three things that were wrong

Each one changed a contract, so each one is in the changelog rather than quietly patched.

  • Ingredient use multiplied instead of scaling. The first implementation multiplied the full ingredient list by every portion eaten, so the pantry drained faster than the kitchen did. Ingredient use now scales by batch yield.
  • Pantry matching used name substrings. Unrelated ingredients collapsed into one stock item whenever a name contained another. Matching now uses exact canonical keys, and an unknown name gets a stable key derived from it rather than joining an existing row.
  • Unknown quantities claimed nutrition anyway. A quantity that could not be converted used to borrow an arbitrary per-100 gram value. It now keeps its missing state, its source and its confidence, because an uncertain number presented as fact is worse than a visible gap.

Incident

When hardening met a dependency upgrade

Trade-off

Gunicorn 25.1 and later open a control socket under the home directory when XDG_RUNTIME_DIR is unset. On a read-only root filesystem that write fails, and the container logged an error on every single start.

The quick fix was to make the root filesystem writable again, trading a security property for a log line. The fix that shipped points XDG_RUNTIME_DIR at the temporary filesystem that is already writable, so the process gets its socket and the read-only root survives the upgrade.

It is a small bug with a useful shape: hardening is not free, and the cost tends to arrive later, through a dependency that assumed a writable home directory.

Boundary

What it deliberately is not

Scope

It is not multi-user, not social, not public-facing, and not a medical nutrition system: its estimates are not medical advice. Each exclusion removed a class of work that would have competed with the part that matters, which is an inventory that stays correct under undo.