Build plan · revision 5 · August 2026
Meal planning, the larder, the family agenda, lists, reminders and spending — in one place. Web first, so it works on a laptop the day it exists; native later, if and when it earns it. This is the shape of the thing, the order to build it in, and an honest account of what is still unknown.
The thesis
The reason to merge these apps is not that five icons is one too many. It is that the interesting information lives between them, and no separate app can reach it. A meal plan is worthless without knowing that Thursday is swimming. A shopping list should know what is already in the cupboard. And a quarter's grocery total means more sitting next to the quarter you started cooking from a plan.
The larder makes the point sharpest. As a standalone app it is a chore nobody sustains; sitting between a shopping list and a meal plan, it mostly fills itself in, because those two already know what came in and what went out. Features that would be tedious alone become nearly free in company — which is the actual argument for one app rather than five.
Settled
| Decision | Chosen | Why this one | What it costs you |
|---|---|---|---|
| First client | Responsive web app | Wanting the big screen changes the order of work. A web app covers laptop, desktop, tablet and phone-in-a-browser on day one, installs on a home screen as a PWA, ships to both of you without an app store, and — because the domain layer is TypeScript — reuses the planning and larder code already written rather than reimplementing it. | No home-screen widgets, weaker notifications on iOS, and offline needs a service worker rather than coming free with the platform. |
| Native apps | Android after the web app earns it | Deferred, not dropped. Build it when there is something you both use daily and the missing widget or notification is a real irritation rather than a hypothetical one. | A second UI to maintain when it arrives. Kotlin Multiplatform stops being the obvious choice once the shared logic already lives in TypeScript — worth re-deciding at the time. |
| Backend | Supabase | Real Postgres. Spending analysis and recipe queries are relational problems, and row-level security maps cleanly onto "one household sees its own data". Self-hostable later. | No offline story out of the box. This is the single largest open risk — see below. |
| AI model | Either — chosen by an environment variable | Claude Opus 5 for prototyping, Gemini 3.7 Flash for running costs and Google-ecosystem tidiness. Both sit behind one interface, so this stopped being a decision that has to be made now. Gemini is roughly seven times cheaper per plan; Claude is the better instruction-follower on a first pass. | Two SDKs to keep installed, and their schema dialects differ enough to need a translation layer. About a hundred lines, already written and tested. |
| Grocery prices | Not modelled; shown live when choosing a product | Pack sizes travel between shops well enough to be worth knowing; pack prices do not. A total that is confidently wrong is worse than no total, so the list says what to put in the trolley and stops there. The pack solver now minimises leftovers instead of pounds, which is the objective that survives changing supermarket. The one exception is the local-only Basket panel, which shows Tesco's own shelf price and price per kilo beside each product while you choose between them. It is never added up, never used to choose, and a remembered price is always shown with the date it was seen. | The budget guard rail goes with it — eight hard checks rather than nine. Frugality becomes a steer in the prompt rather than a rule in code. |
| Where the AI runs | Server-side Edge Functions | The API key never ships to a device, and prompts can change without an app release. | A network round trip, and a second language (TypeScript) in the stack. |
| Region | United Kingdom | Open Banking is mature, so bank connections are a solved problem rather than a screen-scraping project. | Little, now that the money side is category totals rather than itemised receipts. The absence of a supermarket price API stopped mattering the moment prices left the design. |
Architecture
The app is a thick client over a local database, talking to Postgres for sync and to an Edge Function for anything that needs a model. Putting the model call on the server is not a detail — it is what makes the key safe, the prompt editable, and the caching worth having.
Credentials
Calendar does drag Google into the login flow, and moving the model to Gemini keeps it to one vendor. But the two credentials are not the same thing and cannot stand in for each other: a family member's sign-in token reads their calendar and cannot call Gemini, and the Gemini key is yours, billed to you, and cannot read anyone's calendar.
What is genuinely shared is the container. One Google Cloud project, one OAuth consent screen, one billing account, one place to revoke things — which is a real simplification, just not a single key.
GEMINI_API_KEY, held only as an Edge Function secret. It never reaches a phone.The open risk
The worst case for this app is mundane: two people in the same supermarket, one signal bar, both ticking things off the same list. Supabase gives realtime sync when you are online and nothing at all when you are not, so the app cannot treat the server as the source of truth for reads. Every write has to land locally first.
New feature
Every kitchen-inventory app dies the same death, and it is worth being clear about why before designing around it. Entering data costs effort every single time, while the payoff arrives only occasionally. Depletion is invisible — you know when you bought the onions, you never record using half of one — so the count drifts wrong within days. And a confidently wrong larder is worse than no larder at all, because it tells you that you have flour when you do not, and you find out at the point of no return.
So the design goal is not a complete inventory. It is to be usefully right about the handful of things that change a decision, and honest about the rest. Three ideas carry it.
| Class | The question you actually ask | So it is tracked as |
|---|---|---|
| Staple | "Have I got any?" — nobody wants to count grams of flour. | Assumed present. One tap to say it has run out. |
| Ambient | "How many left?" — tins, pasta, rice. Worth counting, never rots. | A number, with no expiry pressure and a long patience for staleness. |
| Fresh | "Will it go off before I use it?" — where the waste and the money actually are. | A number and a date, predicted from the plan, flagged when the week will not use it up. |
| Frozen | "What is even in there?" — the one place a real log pays for itself, because you cannot see in. | An explicit list of portions, mostly written by the planner rather than by you. |
Order of work
Sequenced so that each phase ships something usable on its own, and so the riskiest infrastructure is built while the feature riding on it is still simple.
Deliberately unglamorous. The shopping list is the highest-frequency thing you will both touch and the lowest-risk place to get sync wrong, so it is the right feature to discover the hard problems on. Everything after this inherits the answer. The task logic is now built and tested ahead of the sync layer, which was the cheap order to do it in: the rules are pure functions and did not need a database to be got right.
Most of this exists and runs. What remains is moving it from an in-memory prototype onto Postgres, and filling out the catalogue — data entry rather than invention.
Brought forward, because the Google setup was done and the payoff was sitting right there. The rest of this phase is breadth — more calendars, a nicer view — rather than the hard part.
Deliberately coarse, and much easier than it first looked. The question is "what did we spend on food this quarter, and is that more than last?" — not "what did the tin of tomatoes cost". So there is no receipt scanning, no itemisation and no dependency on a supermarket API. Bank data already carries merchant, amount and date, which is everything a category total needs. Still last, because it is the only phase whose access you do not fully control.
household_id. A future module (chores, medication, the dog's
vet dates) should then be additive rather than surgical.
Prototype
Built and running in prototypes/meal-plan, now with a web
front end at npm run web. The premise was that the hard part
is not getting a model to write an appealing menu — that is easy — but
turning that menu into a shopping list that is actually correct, and
cooking it on the evenings the family actually has, for the people who
are actually there. It now carries the jobs list too, for the same
reason: the difficulty there is not storing a to-do, it is knowing when
the next one is due and whether the week has room for it. A hundred and
sixty tests, all passing, none of which need an API key, because the
parts most likely to be wrong are the deterministic ones.
ingredientId to an enum of catalogue ids, so the model selects rather than invents. This is the single highest-leverage decision in the whole pipeline.npm run web for the browser version, or
npm run demo for the same week in a terminal.
npm run guardrails watches a bad plan get taken apart.
Risk register
| Risk | Status | Where it stands |
|---|---|---|
| Larder drifts out of trust | Open | The real test is whether anyone confirms anything after week three. Designed against — nothing breaks when the larder is ignored, it simply goes stale and buys the items — but only living with it will show whether the confirm prompt lands at a moment anyone is willing to answer. |
| Web app on iOS | Open | Installable web apps are second-class citizens on iPhone: notifications are limited and the install flow is obscure. Fine for a laptop-first tool used by two people who can be told how; the thing most likely to eventually justify a native app. |
| No Android toolchain | Deferred | Still no JDK, Gradle or Android Studio on this machine. No longer blocking anything, because the first client is a web app — this only matters again if and when the native decision is revisited. |
| Offline sync on Supabase | Open | The largest design risk. Mitigation is designed but unbuilt: local-first writes, an outbox, and merge rules chosen per table. Build it in Phase 1 while the only client is a shopping list. |
| Compose Multiplatform on iPadOS | Open | Production-viable on iPhone; large-screen adaptive layouts are the weak spot. Prove it with one real screen early rather than discovering it at Phase 3. |
| Open Banking eligibility | Open | UK aggregators differ on whether an individual developer can hold a live connection or whether a regulated entity is required. Confirm terms with a provider before Phase 4 is scoped — do not assume. |
| Grocery price data | Dropped | Was the second-worst risk in this register: no supported UK supermarket price API, unofficial endpoints that break without warning. Removing prices from the design removed the risk outright. The list now says what to put in the trolley, not what it will cost. The local Basket panel does show Tesco's live shelf prices while choosing products, over the same unofficial session that fills the basket. If that breaks, the panel loses a column. Nothing else reads a price. |
| Model invents ingredients | Closed | Enum-constrained schema. Cannot happen. |
| Wrong quantities on the list | Closed | Unit reconciliation and pack solving are implemented and tested, and refuse to guess when a conversion is impossible. |
| An allergen reaching the table | Closed | Checked in deterministic code against every ingredient of every recipe, not left to the prompt. |
| Google on the critical path | Open | Login, calendar and meal planning now all depend on one vendor and one Cloud project. Accepted deliberately for the simplicity; mitigated by keeping the model call behind a thin interface, since validation and the shopping-list engine are already provider-agnostic. |
| Prompt caching | Open | Explicit caching is not available on the Interactions API. The stable rules and catalogue are placed first so implicit caching can match, but that is a hope with a meter on it rather than a guarantee. Measure before assuming. |
| AI running cost | Closed | One plan per household per week on a Flash-tier model at $0.75 in / $3.75 out per million tokens. Small change even if caching never engages and every plan needs two repairs. |
Next
npm run web and actually use it for a week — the shop and the jobs. The question is not whether it works, it does, but whether either of you confirms anything in the larder after day three, or does the job on the evening it was put on.
the real test
household, person, list_item and task tables with row-level security. person needs a nullable link to an auth user, since most profiles will never sign in. Do this before the next one — its callback URL is built from the project reference.
starts phase 1
Revision 7 · profiles, and a week the family confirms rather than
accepts · 160 tests passing · run it with npm run web