Build plan · revision 5 · August 2026

One app for the whole household

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.

Web first TypeScript Supabase / Postgres Claude or Gemini Google Sign-In Android to follow UK

The thesis

Why one app beats five good ones

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

Decisions locked in

Four choices that constrain everything else
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

Where the work happens

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.

server boundary — API key lives here App Compose Multiplatform Edge Function prompt + schema validate + repair Gemini 3.7 Flash response_format schema Postgres catalogue · pantry · plans constraints plan + list rules + catalogue plan JSON reads catalogue writes plan repair ×2 if invalid
One meal-plan request. The model never sees the raw household record and never gets the last word: the Edge Function validates the returned plan against hard constraints in ordinary code, and hands violations back for a targeted repair before anything reaches the family. Repair turns continue the same conversation by id rather than resending the plan.

Credentials

Two Google credentials, one project

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.

one Google Cloud project Priya on her phone Supabase Auth holds refresh token Edge Function holds server key Google Calendar her events only Gemini 3.7 Flash billed to you sign in her token as her as you
The same Edge Function makes both calls, wearing a different hat each time. Getting this backwards is the mistake that leads to shipping a model key to the client, or to trying to read a calendar with it.

The family's credential

  • Google Sign-In, requested once, covering identity and the calendar scope in the same consent screen.
  • Supabase Auth supports Google as a provider natively and hands you the refresh token, so this is configuration rather than code.
  • Per person. Revoking it costs that person their calendar sync and nothing else.
  • Means nobody has to make an account — they already have one.

Your credential

  • A GEMINI_API_KEY, held only as an Edge Function secret. It never reaches a phone.
  • Not tied to any family member; every plan in every household bills to you.
  • Rotate it without anyone signing in again.
  • If this ever needs to move to Vertex AI for quota or data-residency reasons, it becomes a service account — same idea, different envelope.
The bill for consolidating. Google is now on the critical path for login, calendar and meal planning at once. That is a fair trade for the simplicity, but it means one suspended account takes out three features rather than one. Worth keeping the model call behind a thin interface so swapping it is a day's work, not a rewrite — the validation and shopping-list layers are already provider-agnostic.

The open risk

Offline is the hard part, not the AI

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.

on the phone — never waits for a network Tick an item aisle 7, no signal SQLite source of truth Outbox queued ops Postgres merge by rule Her phone already at the till 1 write 2 queue 3 on reconnect flush 4 realtime add/remove: union with tombstones checked: last write wins, per item
The shopping list is unusually friendly to this: adding and removing are set operations, and "checked" is a single flag per item. Two people ticking different things merge without conflict; two people ticking the same thing agree anyway.
Recommendation. Hand-roll the outbox for Phase 1 rather than adopting PowerSync or ElectricSQL on day one. List semantics are simple enough that a general-purpose sync engine is a large dependency bought to solve a small problem — and building it yourself means you understand the merge rules when the agenda and the ledger need their own. Revisit the moment a feature genuinely needs arbitrary offline SQL.

New feature

The larder, without the data entry

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.

Shopping list what came in Meal plan what goes out A person rarely Projected stock bought − eaten ± corrections Trusted subtracted from the shop Stale bought anyway + bought − eaten corrections only checked recently older than that
One: derive, don't ask. The two flows that bracket the cupboard are already in the app. Stock is mostly arithmetic on them, so manual entry exists to correct drift rather than to create the record. Two: say when you don't know. A figure only reduces the shopping list while it is recent; after that the app admits it is guessing and buys the thing regardless.
Three: different things deserve different treatment
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.
The freezer log writes itself. The planner already knows Tuesday cooks eight portions and only six get eaten, so it can offer "two portions of the stew — freeze them?" without anyone typing anything. That is the whole thesis in miniature: the data was already there, it just needed asking for at the right moment.
When in doubt, buy it. The costs here are lopsided. Over-buying a bag of carrots wastes 85p; trusting a fortnight-old guess and being wrong wastes the meal and the evening. So a stale figure is never subtracted from the shopping list — the app would rather you had a spare onion than an accurate spreadsheet.

Order of work

Four phases

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.

01

The spine — households, sync, lists and tasks

  • Google sign-in, household creation, invite your wife, row-level security
  • Local-first writes in the browser via IndexedDB, an outbox, merge rules, conflict tests
  • Done: tasks and reminders — recurrence with both anchors, diary-triggered reminders, and jobs fitted into the evenings the calendar leaves
  • Shared shopping list, and the task list made multi-user rather than single-household-in-memory
  • Deployed as an installable web app — both laptops, both phones, no app store

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.

02

Meal planning and the larder

  • Ingredient catalogue and recipe tables in Postgres
  • The planner Edge Function — already prototyped, see below
  • The larder: derived stock, staleness, the freezer log
  • Plan review screen: swap a meal, regenerate a day, accept

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.

03

The family agenda

  • Done: Google Calendar read, per-person profiles, and the week's table — who is in for dinner, who cooks, and how long they have — proposed from the calendars and corrected by hand
  • School and club ICS feed subscriptions alongside the Google calendars
  • One merged week view with per-person colour
  • Clash detection, and a plain-English summary of the week ahead
  • Move the calendar read server-side, so the agenda refreshes overnight rather than only while somebody has the app open

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.

04

Money, at category level

  • Bank connection via an Open Banking provider, read-only
  • Transactions sorted into categories — groceries, eating out, fuel, clubs and lessons, utilities — with corrections that stick
  • Spend per category per month, quarter and year, and the comparison between them

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.

Leaving room for "probably more". Keep every phase to the same shape — a table or two, an Edge Function, a screen, and rows carrying household_id. A future module (chores, medication, the dog's vet dates) should then be additive rather than surgical.

Prototype

What the meal-planning spike settled

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.

Proved

  • Hallucinated ingredients are structurally impossible. The response schema constrains 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.
  • Units reconcile. "2 tbsp olive oil" and "60 ml", "2 onions" and "200 g" collapse into one line via per-ingredient density and unit mass — and a conversion that cannot be done fails loudly instead of producing a plausible wrong number.
  • Pack sizes beat rounding up. 800 g of chicken is three 300 g packs, not one 650 g plus one 300 g — 50 g less left over. With prices gone the solver minimises what ends up in the bin instead, and ties break towards fewer packs to carry.
  • Leftovers are cooked twice and bought once.
  • Bad plans do not reach the family. A deliberately broken week trips nine distinct violations — a nut allergy, a four-hour braise on a Wednesday, chicken four nights, a missing Sunday — each with a repair instruction written for the model to act on.
  • The larder works end to end. Confirming six carrots flips them from stale to trusted and drops them straight off the shopping list, live, in the browser.
  • The model is genuinely swappable. Claude and Gemini sit behind one interface with a schema-dialect translator between them. The planner now runs its full loop against a fake provider with no SDK and no network at all, which is the proof that the seam is real rather than aspirational.
  • One domain layer serves both sides. The web app computes nothing itself — every figure on screen comes from the same TypeScript modules an Edge Function would call. That was the argument for going web-first; it now has a working demonstration rather than a paragraph.
  • The calendar changes what gets cooked. Each evening gets its own budget from the longest free stretch between commitments, so a Monday with a school run at 17:00 and a call until 20:30 reports zero minutes and rejects the 45-minute traybake by name. The blanket "45 minutes on a weeknight" rule is now only the fallback for households with no calendar connected.
  • Time zones are handled properly. Wall-clock times are read in the household's zone rather than the server's, so British Summer Time does not silently hand everyone an extra hour of cooking for half the year.
  • Recurrence knows what it is measuring from. Bin day is fixed by the council and putting them out late does not move next week; clean sheets restart their fortnight from when you actually did them. One rule for both is the ordinary bug here, and it is the reason people stop trusting a chore app. Missed fixed occurrences roll forward rather than stacking, so three skipped bin days read as "last Tuesday", not "twenty-one days late".
  • Reminders can hang off the diary instead of a date. "Pack the swimming kit" fires the evening before whatever the calendar calls swimming, and says nothing at all the week the lesson is off. A weekly repeat would have nagged regardless — which is precisely how people learn to dismiss reminders without reading them.
  • The household is people, not a portion count. Each person has a profile — age bracket, allergies, likes and dislikes, whether they can cook, and optionally a Google account. Portion size and cooking ability are derived from the bracket and then overridable, because asking a parent to type a portion multiplier for a toddler is the kind of question that gets an app closed. A teenager eats more than an adult and a baby eats nothing from the pot; both fall out of the same table.
  • Being busy is not the same as being absent. The obvious rule — their calendar covers dinner, so they are out — is wrong in the way that costs trust: a work call from 18:00 to 20:30 blocks the whole evening while the person sits at home, hungry, in the next room. Busy means somebody cannot cook; it says nothing about whether they will eat. The two are answered separately, so a blocked evening takes you off the rota and leaves you at the table. Absence has to be stated — an all-day entry that puts you elsewhere, or a meal plainly somewhere else — never inferred from a full diary.
  • Portions follow who is actually in. The Thursday one child is at a friend's house is 2.6 portions, not 3.2, and the shopping list is smaller by exactly that much. The cooking limit belongs to the person cooking rather than to the evening, which is the only version that makes sense — one adult being out until eight says nothing about the other adult's ability to cook at six. And an evening with nobody free to cook is a real answer the planner is given, because the right response is the freezer, not a faster recipe.
  • A correction outlives the calendar that contradicts it. The grid is a proposal: the calendars fill it in, the family overrides whatever it got wrong, and overrides are stored as a separate layer rather than as edits in place. A refresh that disagrees loses. Verified by re-reading a calendar that had newly marked a child away and watching the human "she's coming home" survive it — with a one-click way to hand the cell back.
  • The week's jobs are fitted, not just listed. Each evening's spare time is measured as a total rather than the longest stretch, because ten minutes before the school run and ten after really is twenty minutes of tidying — while cooking, which cannot be interrupted, still gets the unbroken run. Dinner comes out of one person's evening rather than both, the cooking rota alternates, and unassigned jobs go to whoever has the most left. Work that will not fit anywhere is stated plainly instead of rolling to tomorrow forever.

Still unknown

  • How often the repair loop is needed. If most plans pass first time it is cheap; if most need two rounds, the constraints want loosening or the prompt wants work. Not worth blocking on — a model writing a decent week is already established ground — but worth watching once real weeks start running.
  • Real cost and latency per plan. The runner prints token counts and a cost estimate; the numbers just need collecting. Back-of-envelope is around three cents a week per household, but that is arithmetic, not a measurement.
  • Whether caching engages at all. Explicit caching is not offered on the Interactions API, so the rules and catalogue are merely placed first and left to implicit caching. The runner reports the cache hit rate; if it sits at zero, the prompt is being paid for in full every week.
  • Whether the Sunday-night review actually gets done. The whole grid rests on somebody spending two minutes confirming it once a week. If that does not happen the app falls back to the calendars' guess, which is decent but not right — and a plan built on a stale guess is exactly the kind of thing that gets quietly abandoned. Worth watching whether the review is a pleasant ritual or a chore, because the answer changes how much the week should depend on it.
  • How to read a second person's calendar. Each profile can hold a Google account, and one signed-in account fills its own row. Filling everyone's row means either every adult signing in on their own device, or shared calendars — and children mostly do not have calendars at all, so their rows will stay hand-set. The design handles all three, but which one a real family tolerates is not yet known.
  • Whether anyone tolerates being scheduled. The app now has an opinion about which evening you hoover on. That is either the most useful thing it does or the most irritating, and no amount of design settles which — it needs a fortnight of being ignored or followed. The escape hatches are built in (defer a day, or simply do it whenever) but the honest position is that this is a behavioural bet, not a solved problem.
  • Whether losing the budget check costs anything. Frugality is now a sentence in the prompt rather than a rule in code. That is the honest trade — a budget assertion built on prices we cannot trust would fail plans for the wrong reason — but it does mean one fewer guard rail, and only real weeks will show whether the model needs the discipline.
Run 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

What could still go wrong

Ordered by how much rework the risk causes if it lands late
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

Five things, in this order

01 Run 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
02 Create the Supabase project, and sketch the 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
03 Set up Google Cloud for Calendar access — done, and the runbook is kept for when the second household member signs in. complete
04 Agree the merge rules for a shared list — including what happens when one of you deletes an item the other has just ticked. decide before coding
05 Email an Open Banking provider and ask, plainly, whether a household of two can hold a live connection. phase 4, but slow

Revision 7 · profiles, and a week the family confirms rather than accepts · 160 tests passing · run it with npm run web