← All privacy impact assessments
PIA: Cloud inference — where a group's words are processed, and what stops the bill
Amended 1 November 2026 — Device-only retired (privacy-levels §3E). From the move date, a group that had Device-only runs its AI at Vertex
globallike every other group (§12). No level is left that keeps a group's words away from a model.
Status: Draft — written before we built this change, then revised against the finished
implementation the same day (9 September 2026), and revised again on 10 September 2026.
Written: 9 September 2026 · Last revised: 10 November 2026 (D164: account A$4/day and A$12/seven UTC days; five pausing slices A$5; the A$10 platform day remains binding); 31 October 2026 (§8.7: the two smallest slices raised to A$1.50, and the ledger refuses when it can't answer); 31 October 2026 (§6 and §8.7: the A$200 month, the kill switch at A$300 and a per-account cap; §14: the per-account daily spend record); 28 October 2026 (§12, amendment: photos and PDFs attached to the assistant); 4 October 2026 (§12: every group's AI now runs at Vertex global; this supersedes the Sydney routing in §1–§9 wherever they disagree)
Nothing here is legal advice. Children's Code citations are to the exposure draft;
section numbers may move on registration (due 10 December 2026).
1. Purpose and scope
Every generative call this app makes today goes to OpenRouter, which routes to whichever of
Google's regions it likes. This change moves the calls that carry a group's own information to
Google Cloud Vertex AI in australia-southeast1 (Sydney), and puts a layered ceiling under
the spend so a runaway costs money for hours rather than for a month.
In scope: which surface talks to which provider in which region; what the request body carries when it crosses a border; the spend ledger, its ceilings and the kill switch. Out of scope and covered elsewhere: what the assistant is allowed to see (see our assessment on child accounts and the in-app transparency page), transcript encryption at rest (see our encryption plan), and the on-device dashboard brief, which does not leave the phone at all and is unaffected.
One sentence for why: a group's information should be processed in Australia, and a surface that holds none of it may stay global.
2. Data involved
Nothing new is collected. What changes is where the same request body is processed.
| Surface | What its request body carries | Strictly necessary? |
|---|---|---|
| Assistant turn (router + sub-graphs) | the member's message, the last few turns, group context: member names/roles, the group's date and time, and whatever the bound tools read back | Yes — it is the question and the facts needed to answer it |
| Task/event allocation | task titles, member names, effort levels, existing calendar rows | Yes — the allocation is over exactly these |
| Baby recent summary | a counted digest of one baby's day (5 feeds, never a characterisation) |
Yes, and only behind show_recent_summary |
| Voice baby-log extraction | one spoken sentence about a baby | Yes — there is no other way to read a free-text spoken log |
| Dashboard insight + expiry estimate | the day's items and, behind consent, baby cues | Yes |
| Home brief editions (planned, added 29 September 2026) | one group's fact sheet — at most 30 short facts: task and event titles, times and assignees, linked preparation titles, meal and shopping state, and up to four recall facts; no chat, notes, places, private plans or baby lines; names replaced on the global route | Yes — see our home brief assessment §2 |
| Schedule time lookup (embeddings) | ~150 of the group's own past event titles | Yes |
| Review moderation | a public recipe review the author is publishing to every user | Yes — and it is not group information |
| Tool codegen | our own database schema, at developer time | Yes — no user data at all |
The last two are the only ones that stay global, and the reason is the same for both: there is no group's information in the request. A review is written to be published to strangers; the codegen prompt is a database schema and a TypeScript file.
Baby records, a child's name and a child's day are the most sensitive things in any of these bodies, which is why the health-vocabulary pin in §3 exists.
3. Flows and locations
This table describes the state after the platform default is switched to
INFERENCE_PROVIDER_DEFAULT=vertex — as at 9 September 2026, that switch had not yet happened:
| Node | Provider | Model | Region |
|---|---|---|---|
| supervisor / extraction, calendar, chore, meal_planner, pantry, admin, games, baby, insight, insight_summary, allocation | Vertex AI | gemini-2.5-flash |
australia-southeast1 |
| voice baby-log extraction | Vertex AI | gemini-2.5-flash |
australia-southeast1 |
dashboard insight + expiry estimate (/api/insight, insight_summary node) |
Vertex AI | gemini-2.5-flash |
australia-southeast1 |
task allocation (/api/allocate, allocation node) |
Vertex AI | gemini-2.5-flash |
australia-southeast1 |
| schedule time lookup | Vertex AI | gemini-embedding-001 |
australia-southeast1 |
| organising domains at "Global, records here"; every domain but extraction at "Global, everything" (names replaced) — since 28 September 2026 | Vertex AI | gemini-3.8-flash |
global |
capacity fallback for the gemini-3.8-flash rows — since 22 October 2026 (§12, amendment) |
Vertex AI | gemini-3.7-flash |
global |
| Mail Sifter: the assessment, its pictures and a "Turn into cards" turn — from the release after 31 October 2026 (§12, amendment); the Flash rows above are its capacity fallback | Vertex AI | gemini-4-argon |
global |
| home brief editions (planned) — writer and editor, at Australia, and at a Global level whenever the fact sheet carries health vocabulary, a baby as a task's beneficiary or a baby's name | Vertex AI | gemini-2.5-flash |
australia-southeast1 |
| home brief editions (planned) — writer and editor at either Global level, for a fact sheet with no health or baby content (names replaced) | Vertex AI | gemini-3.8-flash |
global |
| review moderation | OpenRouter | gemini-3.5-flash-lite |
global |
| tool codegen (developer time) | OpenRouter | gemini-3.5-flash-lite |
global |
Moving the dashboard insight and the task allocator onto our assistant server closed both of
the residuals this section used to carry (§8 residuals 1 and 2, below): they now run as
/api/insight and /api/allocate rather than as Supabase edge functions with no runtime
identity, so they follow INFERENCE_PROVIDER_DEFAULT exactly like every other node in the first
row. The deprecated edge functions they replace (generate-dashboard-insight,
ai-task-allocator) were kept deployed, on OpenRouter, global, so that an app version from
before 9 September 2026 could still call them — that window, not a routing decision, is why they
are not in this table. Their code was removed in October 2026, and the deployed copies are
deleted by hand; until each is deleted, an app version that old can still reach it.
Sydney serves exactly one generation model — gemini-2.5-flash — plus gemini-embedding-001
(checked 7 September 2026). It does not serve 3.5 Flash, 3.5 Flash-Lite or 3.7 Flash. Singapore
is as overseas as the United States for the Code's purposes and buys nothing. So Sydney is 2.5
Flash or it is not Sydney.
Cross-border disclosure (APP 8). Today every one of these bodies is disclosed to an overseas recipient. After the switch, the only group-data body that still is, is the dashboard insight — see §8 residual 1, which is the finding of this assessment rather than a footnote to it. The Code's cross-border clause and APP 8.1 both turn on taking reasonable steps before disclosure; moving the processing onshore is the step, and the residual names the one place it was not possible in this change.
Region routing inside one turn. A future privacy setting will let some steps run globally.
This change lays the seams and flips nothing: every turn is tagged with the domain that produced
it (origin), filterHistoryByOrigin is the pure function that will withhold a baby turn from a
global step, and a health-vocabulary pin forces any extracted intent whose text carries
medicine / dose / fever / doctor / vaccine / allergy language onto the Sydney provider whatever
the level says. Today every level resolves to Sydney anyway, so this is a data-shape change; it
is built now because the moment it matters is the moment it is easy to get wrong.
4. Legal basis and consents
No new consent is created and none is needed: the purposes are unchanged and each is already
covered by an existing switch (voice_assistants_enabled, show_recent_summary,
dashboard_insight, improve_app_traces). A stance or a preset may not consent and nothing
here writes a consents row.
The privacy policy's AI section and the in-app transparency page gain one sentence naming the processor and the country. It ships as tested app copy and is released together with the provider switch — not before, because a residency claim that renders while the calls still go to OpenRouter is a false statement in a legal document.
No zero-data-retention claim appears anywhere. Our zero-data-retention (ZDR) request to Google is pending; until it is granted and evidenced, the strongest true sentence we can make is that the provider does not train on the request, which our privacy policy already says.
5. Risks and mitigations
| # | Risk | Mitigation | Kind |
|---|---|---|---|
| 1 | A group's words are processed overseas when the app says they are not | The claim renders only with the switch; an automated check refuses any household-data node resolving to a non-Sydney endpoint; the URL is built from a constant, not from free env | Structural (check + constant) |
| 2 | A baby record reaches a global step once a future privacy level allows it | filterHistoryByOrigin + the health-vocabulary pin, both pure and unit-pinned; no baby tool bound outside the baby domain (asserted) |
Conventional today, structural once that level exists |
| 3 | Runaway spend empties the card | Six layers, §6 | Mixed — see §6 |
| 4 | The ledger stops binding silently (a meter reading zero) | Both usage shapes read; assertMetered logs loudly; an automated check asserts every call site pre-flights and bills outside its try |
Structural (check) |
| 5 | A model step-down makes the assistant worse at obeying instructions | The defaults do not flip as part of this change. We run an evaluation suite against the deployed service and compare obedience and over-split to the 3.7 baseline in our model evaluation notes. If worse: wait for a 3.x Flash in Sydney, or accept a stated overseas surface — never a quiet compromise | Conventional (a person reads a number) |
| 6 | The kill switch fires and nobody knows why | It logs; the next request finds Vertex 403 and the user is told the assistant is paused for the rest of the month; an internal runbook is the re-enable procedure | Conventional |
| 7 | A member's own weekly allowance stops being the first gate | Layer 1 is untouched; the platform ledger is added under it, never instead of it | Structural (the RPC is unchanged) — updated 2026-09-10: a database change on 10 September 2026 merges layer 1's input and output ceilings into one and doubles the total (4,800,000 per user per week). Same RPC shape, same auth.uid() identity, same check-and-charge in one statement; the gate is still first and still structural |
A harm this change does not create and should be named anyway: moving to a single region and a single model means one provider outage takes the assistant down for everybody. Failing closed with a sentence is the right answer for a family organiser, and is what already happens when OpenRouter is out.
6. Structural versus conventional
Structural — Postgres, a project boundary or an IAM grant refuses it:
inference_spend,inference_ceilings,model_prices,platform_reports: no client grants at all. A member cannot read the platform's costs or edit a ceiling; our test suite checks the wording of the refusal itself, not just a bare error code.record_inference_spendis security-definer, its identity isauth.uid(), and it refuses when that is null — the same ruleconsume_agent_tokenshas kept since 7 August 2026, which is what stops a service-role caller from billing a nobody. Check and charge are one statement, so two concurrent turns cannot both squeeze under a ceiling.- The ceilings themselves (since 31 October 2026, our decision "AI ceiling A$200"). The RPC refuses when the platform's
day reaches A$10, a surface reaches its own slice, or the month reaches A$200. The caller cannot decline to notice.
Mail Sifter has its own A$5-a-day slice (
mail_sifter) and no longer shares the Home brief'sinsightslice. - Each account's share is capped in the same RPC. An account's charges count toward the shared pools only up to
A$4.00 a UTC day and A$12.00 over the last seven UTC days (
inference_account_ceilings, service role only). Past that, the account's own requests are refused (account_daily,account_weekly), and nothing more it is charged reaches the shared pools. One account cannot fill an A$5.00 slice; two can pause that feature for everyone (§8.7). One call is refused above 64,000 input or 16,000 output tokens. A surface with no ceiling row is refused. Two calls from one account at once wait for each other on that account's row. What each account was charged is kept for 14 days (§14). - The kill switch disables Vertex on
groupapp-inferencewhen the month's actual spend reaches A$300, 1.5 times the ceiling (it was A$250). It catches spend that escapes the ledger. It doesn't stop TypeSafe's Jev or LangSmith. - The project boundary. Inference bills to
groupapp-inference; the app project holds its own $20 smoke detector. A cap that kills the API cannot take the app down with it. - The kill-switch identity holds
roles/serviceusage.serviceUsageAdminon that project and nothing else, and holds no Supabase credential of any kind. The worst it can do is turn Vertex off. - The Vertex endpoint host is assembled from a pinned region constant. There is no env var that can point a household-data node at another country.
Conventional — a person, a test or a comment keeps it true:
- Which node is on which provider is resolved from an environment setting. An automated check is what makes a change to that visible before it ships; nothing in the running system refuses a bad value beyond the Sydney-host constraint above.
- The copy shipping together with the switch rather than before it: a sentence in the release walkthrough, and us following it.
- The requests-per-minute quota (layer 3) is a console setting that we request. We can compute the number here but cannot apply it ourselves.
- The preview spend cap and the ZDR request: both are console settings, both pending, both ours to action.
- The kill switch's A$300 takes effect only when we apply its infrastructure configuration from the laptop. Until then the deployed function fires at A$250.
7. Best interests of the child
Two things in a child's favour, one honest limit.
In favour. A child's records — their name in a task, a summary of a baby sibling's day, a spoken log — stop being disclosed to an overseas recipient in the ordinary case. And the health-vocabulary pin means a sentence that sounds medical is treated as medical for routing purposes even when the extractor did not classify it as such: the failure mode is "processed in Australia when it did not need to be", which is the direction to be wrong in.
The limit. A child account has no assistant, no daily brief and no voice, so almost none of these surfaces run for a child directly. What runs about a child is an adult's request, and the adult is the one who chose to make it. The dashboard insight residual (§8.1) therefore touches a child's information — a baby's counted cues, behind the group's own consent — and that is stated rather than minimised.
Nothing here evaluates, characterises or scores a child. The baby summary's counted-not- characterised rule — our rule that a baby's day is described in counts, never in a characterisation — is unchanged by a change of provider.
8. Residual risk we accept
Closed: the dashboard insight now runs on our assistant server. The old edge function
generate-dashboard-insightis replaced byPOST /api/insight, which has a runtime identity a Supabase edge function never had. Its cache read/write and its roster, completions and nudge-state lookups all run on the caller's own RLS-bound client; our assistant server still holds no service-role key and never will — giving it one was considered and rejected outright, and was never built. The deprecated edge function stays deployed, unmodified beyond a banner naming the replacement, for as long as an app version from before 9 September 2026 can still call it.- Residual within the closure: the cache write currently fails.
group_dashboard_insightsgrantsauthenticatedSELECT only — it was built for the edge function's service-role writer, and nothing has granted an owner-scoped INSERT/UPDATE. The new route attempts the same upsert the edge function always did, gets refused, logs it, and serves the brief anyway — the same non-fatal handling the edge function already had for a failed cache write, so nothing a user sees breaks. What is lost is the caching itself: this route currently regenerates the insight on every request rather than serving a cached one, which is real spend that the platform ledger (layer 2) and the caller's own weekly allowance (layer 1) still bound, but bound less cheaply than intended. The fix is a follow-up database change — an owner-scoped INSERT/UPDATE grant plus a matching access policy, or asecurity definerfunction in the shape ofrecord_inference_spend— deliberately left for later. This is tracked as a known issue. - The health-vocabulary pin and the Sydney pin both still apply: the dashboard insight step is
pinned to Sydney in the same routing configuration as the baby summary endpoint
(
/api/baby/summary), so this surface comes home whatever a per-node override says once Vertex is the platform default. - One behavioural narrowing, disclosed rather than worked around: a signal for whether a
member has used the assistant recently used to read the
chat_messagestable directly, inside the edge function. Our assistant server can never read that table — the transcript is sealed under the member's own key and an automated check refuses any code that tries — so this signal is now permanentlytrue(a documented fail-closed default for a signal that cannot be read), which permanently suppresses one meal-planning nudge. Cosmetic; the shopping-list nudge is unaffected.
- Residual within the closure: the cache write currently fails.
Closed the same way. The old edge function
ai-task-allocatoris replaced byPOST /api/allocateon our assistant server. AgroupIdfield was added to the request body — the edge function had no notion of which group, authorising by JWT alone — because the planned device-only privacy check is group-scoped and will need one; the deprecated edge function ignores the extra field. Everything else, including the response shape and the deterministic-floor validation rule, is unchanged. Since 18 September 2026 the request may also carryrules.excludedMemberIds— member ids the household has asked to leave out this week, which are already in the members list it sends — and the assignment and the shopping-list unification are two requests rather than one; the server leaves an excluded member out of the roster the model is shown and refuses any answer naming one. No new category of information reaches the model; strictly less does. The response gains a count of refused entries and per-stage timings, numbers only.As at 9 September 2026, nothing here had been run against Vertex. The evaluation run, the first real generation call, the first real ledger row and the first budget alert are all steps we take after deployment.
The kill switch is exercised only by its unit test until a threshold is actually crossed. A Pub/Sub message that does not match the schema-1.0 shape the parser expects would be ignored rather than mis-firing, which is the safe direction and also the one that fails silently.
originis persisted inchat_messages.metadata, not in a column of its own, and every turn written before this shipped has no tag.filterHistoryByOriginADMITS an untagged turn rather than withholding it — withholding would silently shorten the assistant's memory for everybody on a change meant to be invisible. That is today's behaviour exactly, and it becomes load-bearing only when a future privacy level is introduced that is not Sydney; at that point the honest options are to withhold untagged history or to backfill, and both are decisions rather than defaults.A single region and a single model is a single point of failure, accepted for the reason in §5.
Revised by founder D164 on 10 November 2026: a member who calls
record_inference_spenddirectly can use only their own allowance, but two accounts can fill a small slice for everyone. The RPC is still granted toauthenticated, because every surface bills on the caller's own client and our assistant server holds no service-role key. So a member can still post token counts for calls that never happened. Since the migration of 31 October 2026, with limits revised by founder D164 on 10 November 2026:- each account's charges count toward the shared pools only up to A$4.00 a day and A$12.00 over seven days;
- one call is bounded at 64,000 input / 16,000 output tokens (it was 2,000,000 / 200,000). Our assistant server splits a larger settle into bound-sized charges, so no real request is refused by the bound;
- a surface the ledger doesn't know is refused.
A hand-made loop now uses up its own account's cap and is then refused, like any other request from that account.
What remains for the beta, with sign-up capped at 150 accounts: the earlier limits were reviewed by Security and accepted by the founder on 7 November; founder D164 subsequently raised them on 10 November. This is about availability only. A fabricated charge moves no data, reveals nothing and costs us no money: it uses up the ledger's count, not the model.
- Any signed-in account can do this, in a group or not. It takes several calls and costs the caller that day's allowance. One person can hold several accounts, so "accounts" below need not mean several people.
- The caller names the surface, so two accounts can fill a small slice and pause that feature for
everybody until 00:00 UTC:
embeddings(schedule time lookup, task search and matching a routine by name),baby_summary,mail_sifter,insight(the Home brief and routine suggestions) andallocationat A$5.00 each. voice_extraction(A$1.50) can be filled too, but pauses nothing: a spoken baby log isn't checked against that slice, so that a parent's spoken log is never lost.- No safety or privacy check depends on a small slice, and a pause lets nothing through. A paused feature sends nothing to a model and writes nothing: an email waits unsorted, a routine is changed only by its exact title, and the Home insight falls back to a plain greeting. The attachment guard and the fast-command check are on the assistant's slice, and when it is full they refuse rather than go on unchecked.
- Two accounts can fill the assistant's A$8 slice and three the A$10 day, fewer on a day with other use. The ledger refuses at the limit, so exact equality is enough.
- Four accounts at their weekly limit (five in a 28-day month) can fill the A$200 month by about its last days. More accounts do it sooner, because the ledger records a hand-made charge even when the day is already full: 25 accounts in two days, 50 in one. All AI then stays paused until 00:00 UTC on the 1st, unless we raise the ceiling.
Capping one account's share of any one surface is left for before the public App Store release.
- The charges are attributable to accounts: the record in §14 shows which accounts reached their cap that day, though
not on which surface. (The text this replaces said the ledger rows named the user.
inference_spendhas never held a user id. Only §14's record does.) - Since 31 October 2026 the ledger and the token meters refuse when they can’t answer (an error, no answer, or a refusal of their own), so no model call starts that the ceiling hasn’t just admitted. If the ledger fails during a turn, the step already under way can finish uncounted and the turn stops at its next check. Our two production switches are the way through during a long outage.
/api/insightand/api/allocateread the privacy-level ladder only far enough to refusedevice_only. Both are eligible for the global provider atglobal_all(the same eligibility rule as the organising domains and the baby summary), but neither replaces a household's real names in its prompt the way/api/baby/summarydoes — a step that swaps names out of the prompt and restores them afterwards. Wiring the global level into these two routes without that replacement would send a household's real names to a global endpoint the moment someone choseglobal_all— the exact disclosure that name replacement exists to prevent — so both routes stay Sydney-only regardless of a non-locked level, which costs slightly more and leaks nothing. An automated check pins the absence as deliberately as it pins the refusal.
9. Review trigger
29 September 2026: the third stage of the personal Home and assistant widgets changes no route — assessed against its design, and checked again the same day against it as built. Our Home, widgets and setup assessment §10 describes it, and its review triggers ask for this document to be revised if general conversation were given its own place in the routing between Australia and the global provider. It is not:
- General conversation has been answered since 28 September 2026 by one model call, with no lookup, on the extraction step — the step pinned to Sydney at every level — and it stays there.
- The combined read of tasks, calendar and, newly, meal plans runs on the
insightstep, as the combined read of tasks and calendar already does: Sydney at Australia and at "Global, records stay here"; at "Global, everything", Vertexglobalwith names replaced, unless the request carries health vocabulary — the rows already in §3. - The new current-meal-plan-day lookup is bound to the meal planner, which already goes to
Vertex
globalat either Global level with names replaced; it reads no baby or health table. - A request to open a tool or show a view is answered from a fixed table after extraction, with no further model call; a request to see a baby's growth chart still runs the existing growth-chart step, on its existing route.
No new node, provider, region, model or model call is added. What changes is how much of the group's records one question can carry on those existing routes, and it is capped per question at 6 lookups, 50 rows of one kind, 120 rows in all, a 31-day window and 32 KiB of text. No price row, ceiling or kill-switch setting changes.
29 September 2026: the home brief, redesigned (planned, not built). Assessed and accepted in our home brief assessment. What it changes here:
- A new surface that follows the Global levels. One paragraph per group, twice a day, written
by a writer pass and at most one editor pass. At Australia both passes run in Sydney on 2.5
Flash. At either Global level both run at Vertex
globalon 3.8 Flash with names replaced, the same as the organising domains above — unless the fact sheet carries health vocabulary, a baby as a task's beneficiary or a baby's name, in which case both passes run in Sydney. Before each pass the same text goes to the same endpoint's token-counting service, at most twice. No other provider: a route that resolves to anything else writes nothing. - It is paid for by the group, not by a member. The brief does not charge a member's weekly
allowance (layer 1 in §6). It is bounded instead by a per-group weekly limit of A$0.50, held and
checked in the database under a per-group lock before every provider attempt, with the platform
ceilings (layer 2) unchanged over it; the brief's platform spend is counted under the existing
insightsurface. Becauserecord_inference_spendtakes its identity from a member's session, the brief's own database functions write its platform totals and check the same daily, monthly and surface ceilings. - §8.8 is unchanged for today's route.
/api/insightstays Sydney-only and keeps serving app versions from before the redesign until they have gone; the redesigned brief is a separate route that does replace names, which is the condition §8.8 set for going global.
28 September 2026 model and route review. Three changes, each written down before the code.
- Sydney goes back to Gemini 2.5 Flash for every node. 3.5 Flash in Sydney proved unreliable in practice: a side-by-side probe from our inference project on 28 September found it answering with "out of capacity" on most attempts during the Australian evening, after which our server had been quietly answering on 2.5 Flash anyway. The assistant, summaries, voice extraction and allocation all use 2.5 Flash in Sydney; the backup model and its cooldown are removed, because there is nothing left to fall back to. Processor and region are unchanged: Google, Sydney.
- Global-level groups are processed by Google Vertex AI at its
globallocation, on Gemini 3.8 Flash, instead of through OpenRouter. The same steps go global as before (the organising domains at "Global, records here"; every domain but extraction at "Global, everything"), with names replaced and restored exactly as before and the health-vocabulary pin still winning. What changes is who receives the text: Google directly, rather than OpenRouter routing onward to Google. That removes a party — after this change no group's words reach OpenRouter from our assistant server at all (the deprecated edge functions in §3 keep their pre-9-September window). Vertexglobalmay process in any Google region, which is what the Global levels' own consent screen already says ("any of our providers' regions"); no consent copy changes. 3.8 Flash is not served in Sydney, so this is only possible at a Global level. - The experiment lab calls the same routes from a separate project (
groupapp-evals), with synthetic data only. It shares no bill with production and cannot trip production's kill switch.
Spend: both models are priced before the switch (2.5 Flash Sydney since 9 September; 3.8 Flash
global at Google's standard rate, recorded ahead of the introductory rate so the ceiling cannot
under-count from 2027). Nothing in §6's structural column changes: the region for Sydney is still a
constant, the global location is a constant too, and an unconfigured Vertex is still a refusal,
never a fallback.
24 September 2026 model review. The routing table above records the original decision, when the Sydney endpoint exposed only Gemini 2.5 Flash. A direct call from our inference project now succeeds with Gemini 3.5 Flash on that same Sydney endpoint. The tool-using assistant can therefore use 3.5 while short summaries, voice extraction and task allocation retain 2.5. This changes neither the request bodies nor the processor or region. The new assistant model costs more per token, including reasoning output, so the existing spend ceiling needs its published price row before the model switch reaches production. The assistant tool-choice evaluation is recorded separately. This amendment does not resolve any existing global-provider residual or permit a global fallback for a Sydney call.
Revisit again when Sydney model availability or pricing changes materially; the ZDR request is answered either way; a future privacy level that is not Sydney is introduced; the insight moves onto our assistant server; a ceiling is raised; or the Code is registered and the cross-border clause moves.
10. Checklist against exposure draft s 38(2)
| s 38(2) | Requires | Answered in |
|---|---|---|
| (a) | nature, scope, context, flow and purposes | §§1–3 |
| (b) | why collection is strictly necessary | §2, per surface |
| (c) | lawful and fair means | §4 — no new collection; the change is where processing happens |
| (d) | consistent with the best interests of the child | §7 |
| (e) | how the entity complies with the Code and the Act | §4, §6 (APP 8 in §3) |
| (f) | risk of harm and potential impact | §5, §7 |
s 38(3): written before the code, in the sequence this document sets out, and finalised first. s 39: listed on our published register.
11. Sources
- Australian Privacy Principles, APP 8 (cross-border disclosure) — Privacy Act 1988 (Cth) Sch 1. https://www.oaic.gov.au/privacy/australian-privacy-principles
- Children's Online Privacy Code, exposure draft — cited throughout this register; see
docs/privacy_posture.mdfor the register's own note on draft status. - Vertex AI generative AI locations (which models serve which region):
https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations — and the live
endpoint listing probed 2026-09-07 for
australia-southeast1, which is what this assessment relies on, because the documentation page and the endpoint have disagreed before. - Vertex AI pricing (the per-1M figures seeded into
model_prices, regional 10% above global): https://cloud.google.com/vertex-ai/generative-ai/pricing - Gemini 3.5 Flash availability and regional processing.
- Gemini 3.5 Flash Standard non-global pricing.
- Cloud Billing budget notification format, schema 1.0 (
costAmount,budgetAmount,alertThresholdExceeded): https://cloud.google.com/billing/docs/how-to/budgets-programmatic-notifications - In repo:
terraform/bootstrap/projects.tf(the project, budget, topic and kill-switch identity),supabase/migrations/20260807170000_agent_usage.sql(consume_agent_tokensand theauth.uid()rule this PIA's RPC copies),agent-server/src/embeddings.ts(the ADC-over-fetch pattern),docs/ml_experimentation.md(the 3.7 baseline the step-down is measured against),docs/encryption_plan.md§0.5 (why no function gets a key).
12. 4 October 2026 — every group's AI runs at Vertex global
This section supersedes the Sydney routing described in §1–§9 wherever they disagree. The rest of this assessment — the spend ledger, its ceilings, the kill switch, the separate inference project, "unconfigured is a refusal, never a fallback" — is unchanged.
What changed. Every model call our assistant server makes for a group that is not Device-only
now goes to Google Vertex AI at its global location, which may process in any country where
Google operates, including the United States. The privacy level no longer moves a call: Standard
(the stored values australia, global_records_here, global_all) all route the same way, and
Device-only is still refused before any model is built. Names are no longer replaced with
placeholders before Google sees a request. Health words and baby records are no longer pinned to
Sydney. Models: Gemini 3.8 Flash for the assistant (extraction at low thinking), summaries,
allocation and the Mail Sifter assessment; Gemini 3.5 Flash-Lite for the home brief and the voice
baby-log extraction (both single-shot, neither binds tools).
Why. Sydney could no longer carry the product, measured rather than assumed
(docs/ml_experimentation.md, 4 October 2026):
- Gemini 2.5 Flash — the only model that was dependable in Sydney — retires from Vertex on 20 October 2026 (Google's model page).
- Gemini 3.5 Flash is served in Sydney, but Google lists Sydney only under Provisioned Throughput, not pay-as-you-go. In practice it answered 429 at the Australian evening peak (28 September) and took 13–23 s per call at midnight (4 October), against 2.3 s for 2.5 Flash. Provisioned Throughput is far beyond this app's scale.
- No Flash-Lite model is served in Sydney (both return 404).
- The alternatives that do process in Australia (Claude through Bedrock's
au.profile, Azure's gpt-4.1-mini, SCX's onshore models) mean a new vendor and new credentials before 20 October, while the app is still in testing with a small group of invited people. Our position: be honest that we cannot deliver a good product on Australian-only models today, use Google's global models, and say so plainly in the privacy policy.
What stays in Australia: the database (Supabase, Sydney); the group key service (KMS, Sydney);
search matching (task and meal search embeddings, gemini-embedding-001 in Sydney); consented
traces are stored in LangSmith's Australian region.
Developer-time trace reads (22 October 2026, HQ PIA-003). groupapp-evals also serves Claude
at global for PIA-003's developer-time trace reads; no member request reaches it
(assistant tracing §3).
Second processor. TypeSafe's Jev (United States) gives a second judgment on Mail Sifter
assessments. It used to apply only at "Global, everything"; it now applies to every group that
uses Mail Sifter — still with roster names replaced, and never for an email carrying health words
(agent-server/src/inboxAssessment.ts).
Consent and notice. The Australia/Global choice and its overseas_processing consent are no
longer offered; existing grants simply lapse through the sweep, which now changes nothing. The
privacy policy moves to v3.0 and every adult member sees what changed once (the policy-change
sheet). The young-member AI consent changed its words, so its version moved and every 16- or
17-year-old is asked again (their switch is cleared; the server refuses until they answer). The daily-brief consent's words changed too and new answers record the new
version; adults who already said yes are not asked again — they are told through the policy-change
sheet every adult member sees once for v3.0 (owner decision, 4 October 2026), and can turn the brief
off in Settings.
Children. A child's records, a baby's feeds and health words now reach a model outside Australia when the group is not Device-only. This is the accepted residual of this decision; the protections that remain are the ones that never depended on region: child accounts and their guardian consent, the young-member AI consent, the assistant's medical guardrail, Google's own terms saying it does not train on requests (its promise, which we cannot check), and Device-only for any group that wants nothing to reach a model. Re-check against the Children's Online Privacy Code when it is registered (due 10 December 2026) and before public beta.
Residual risks accepted. (1) Overseas processing of every group's words, including health and children's information, by Google. (2) TypeSafe now sees every Mail Sifter group's redacted emails. (3) Gemini 3.5 Flash-Lite writes the home brief without a quality evaluation — the validator still refuses a draft that breaks its rules. (4) 3.8 Flash's default thinking makes a domain step slower than 2.5 Flash in Sydney was (experiment 001: 12 s median Global turn).
Revisit when: an Australian region serves a capable Gemini model on pay-as-you-go; the
Children's Code is registered; before public beta; a member asks for Australia-only processing
(Device-only is the answer today); or Google designates any model GroupApp calls as a Frontier
or Advanced AI model, or its abuse-monitoring page changes its retention or scope; or Google
changes implicit caching's tiers, lifetime or location, or cacheConfig reverts.
Amendment, 22 October 2026 — 3.7 Flash as the capacity fallback (owner decision of
21 October). When Gemini 3.8 Flash has no capacity at global (a 429, a 5xx, a network failure
or a call that hangs), our assistant server retries it with backoff and then answers with
Gemini 3.7 Flash, at the same Vertex global location, in the same Google project. Same
processor, same location, same terms; the request is identical except for the model's name. So
nothing in this assessment's flows, notice or consents changes, and no privacy-policy change is
needed. It applies only to the assistant's steps, summaries, allocation and the Mail Sifter
assessment (the getModel() callers); the home brief, shopping and the Flash-Lite callers are
unchanged. (Since 28 October 2026 shopping's product check uses it too, with one retry of 3.8
rather than two, before Flash-Lite; our shopping assessment sets out that
change, accepted on 28 October 2026.) The location is still a constant no setting can move, a 403 from the budget kill switch
still fails closed, and each call is priced at the model that answered it
(supabase/migrations/20261022013738_gemini_37_flash_global_price.sql). Claude or OpenRouter as a
fallback would be a different processor and would need its own assessment first.
Amendment, 24 October 2026 — pictures in Mail Sifter (PIA-008). Pictures in Mail Sifter, from
email and from shares, now reach Gemini 3.8 Flash (3.7 Flash on capacity) at Vertex global, as
re-encoded pixels with a fixed instruction. Unlike text, nothing in a picture is replaced: names
and faces go as they are. Same processor, location and terms. Children's faces, including other
families', are an accepted residual (PIA-008 R1, R2, R6). Off behind two platform switches until
the next policy version is on phones. Revisit with on-device text recognition for shares (F7).
Amendment, 31 October 2026 — Mail Sifter on Gemini 4 Argon (founder decision of 30 October;
Security ruling of 30 October, AG1–AG8). Mail Sifter's model calls (the assessment of a
forwarded email or a share, the pictures it reads, and a "Turn into cards" turn's steps) go to
Gemini 4 Argon (gemini-4-argon, the plain model), at the same Vertex global location,
in the same Google project and under the same terms as 3.8 Flash. On a capacity error or a hang
the call falls back to today's Flash route: 3.8 Flash, then 3.7 Flash, as in the amendment of
22 October. Nothing else moves: assistant attachments (PIA-011), the assistant's own turns, the
home brief, shopping and the insight tier stay where they are. The location and the model are
constants no setting can move; each call is priced at the model that answered.
What Google's terms say about logging, as read on 30 October 2026 (Google's abuse-monitoring and Argon model pages, both "Last updated 2026-10-28"):
- Plain Argon is not a "Google Frontier Model". Only its allowlisted Cyber Permissive variant is, and that variant's prompts and responses are kept for 30 days. We never call it, and code checks refuse its name.
- Plain Argon and 3.8 Flash are under the same standard regime. If Google's automated safety
classifiers flag a request, its prompt (a picture included) may be logged for up to 90 days
in the region we chose (
globalnames no country) and reviewed by Google staff. It is not used for training. - Vertex's implicit cache: in-memory, project-only, up to 24 hours, never used for training.
Durable (at-rest) caching is off in our inference project (
EPHEMERAL, set 31 October 2026 by the founder and verified with a read-back; Security's durable-cache ruling of 31 October, on Google's zero-data-retention, data-governance and context-caching pages, all "Last updated 2026-10-30"). Turning it back on needs Security. - We are on the online terms, and no abuse-monitoring exception is in place. So we make no zero-retention claim, for Argon or for Flash.
The privacy policy's list of models gains Gemini 4 Argon in the next policy version (v3.15). Picture reading switches on only once that version is on phones. Until then Argon may answer text, as 3.7 Flash did on 22 October: same processor, location, terms and data.
Revisit when: Google designates any model GroupApp calls as a Frontier or Advanced AI model,
or the abuse-monitoring page changes its retention or scope; or Google changes implicit caching's
tiers, lifetime or location, or cacheConfig reverts. Re-read both pages, and Google's
zero-data-retention page, on the release day that carries Argon and before each picture switch
goes on. If plain Argon is listed, Mail
Sifter goes back to the Flash route until Security rules.
Amendment, 28 October 2026 — photos and PDFs a member attaches to the assistant (HQ PIA-011;
built, switched off). Two new calls, both at Vertex global, same processor, project and terms,
each reserving the member's, the group's and the platform's allowance before it runs and charged
after, under the same budget kill switch, which fails closed:
- A fixed picture call carrying the pictures a member attaches to a message (at most four, two a call), re-encoded so only pixels remain, with Mail Sifter's fixed instruction (words only, never a description of a person) and nothing else, to Gemini 3.8 Flash, falling back only to 3.7 Flash. Nothing in a picture is replaced: faces, other people's names and children's go as they are. No tools are bound.
- A guard call carrying the masked words read from the files (never pixels, the member's sentence, the member list or a file name) to Gemini 3.5 Flash-Lite, with no tools and a fixed verdict, one call a message, no other model.
A PDF is read on our server in Sydney without a model; its words then go to the guard and to the assistant's turn, as typed words do. We make no zero-retention claim for either call; whether request-response logging is off in the production inference project is unverified as at 28 October 2026 and must be recorded before the switch goes on. Recognising text on the phone is the upgrade path that would keep pixels off our server and Google's. The full assessment is the assistant reads a photo or a PDF you attach.
7 November 2026, D151 — queue tier and time limits. Eligible retries can use
Vertex Priority PayGo on the same models, processor, project and global endpoint.
No new data flow, retention or disclosure is introduced. The shared setting ships
off; enable retries in a later release after the conservative price rows are live.
Capable phones can wait longer (48 seconds server, 58 phone); older phones retain the previous schedule, and signed assistant tool steps
now get a 20-second first-attempt cap within the existing 25-second call deadline. No member-facing words or prompts change.
10 November 2026 — consent notice prominence (PIA-007 C28)
The founder's signed decision “consent notices, middle path” of 10 November 2026 amends C28 to require equal prominence: equal button size, text weight and 48 dp targets, with a filled Turn on and an outlined Not now. Neither is pre-selected.
13. 27 October 2026 — adults are asked before their requests reach a model (HQ PIA-007 C25–C31)
What changes. Until now adults were told about AI only in the privacy policy. They will be asked
on a sheet, "Use AI features?", that names the two services a member's request can reach:
Google (Gemini on Vertex AI, global) and TypeSafe (Jev, United States). No other provider
receives a member's request: OpenRouter was taken out of our assistant server in October 2026, and
the two edge functions that still called it are retired (below).
One gate on the server (agent-server/src/aiConsent.ts). Every agent-server route that can
reach Vertex, TypeSafe, voice or Gemini Live asks one check, once, before anything is read for or
sent to a model. A child is refused, including a child with no birth month on record who is linked
to a guardian or known to be a child. A 16- or 17-year-old needs their own young_member_ai yes, as
before. Everyone else needs their adult_ai switch on and, in the consent ledger, a current
assistant yes at AI_DISCLOSURE_MIN_VERSION or later. The shared display and profile accounts
aren't asked: their AI runs under the admins' group consents, as does Mail Sifter's server sorting
(C31). An account with no birth month that isn't known to be a child is treated as an adult, as
it always has been, and is asked as one (our decision, 27 October 2026: refusing them
would cut off existing adults). An explicit yes to the daily brief doesn't stand in for the AI yes.
Every read goes through the caller's own client, on every request, and fails closed; a missing
switch row is off. agent-server/checks/aiConsent.check.ts lists every route agent-server
registers and fails when one that can reach a model doesn't ask the gate first.
Inert until the founder raises the minimum (C26). The adult part refuses nobody until
ai_disclosure_enforced() is true: the founder has set ai_disclosure_settings.enforce_from_app_version
to the first app version with the sheet, and the iOS and Android min_supported_client_version are
both at or above it. Until then an adult's AI works as it does today. The server reads the
founder's value itself, so only a value it has read as unset keeps the gate off; once a value is
set, a check it can't complete counts as on.
When it is on, an older app's refusal reads "Update GroupApp to keep using AI features".
An adult's yes doesn't lapse (F9). The Children's Code's twelve months are for children. An adult's grant carries no expiry (a constraint refuses one with an expiry), so the sweep never touches it. Adults are asked again when a provider, region or kind of data changes: a new version and a new server minimum (C30c).
The sheet (C27–C29). From the privacy policy version that describes it (v3.14), the app asks
"Use AI features?" of every account not known to be under 18, an account with no birth
month included, since the server treats that account as an adult; never a child, the shared
display or a profile, and not a member whose only groups are Device-only. It shows on the
Assistant tab, as a card in first-run setup after an adult birth month and a first Standard group,
and before the first request of any AI feature the member taps, so nothing is sent before the
answer. Turn on writes the member's adult_ai switch and then an assistant consent row at
ai-disclosure-1.0-2026-11, with no expiry. Not now writes nothing and sends nothing; the
sheet comes back at the next AI tap, and features that run on their own fall back to their non-AI
versions. You → Your choices has an "AI features" row with the date of the yes; one tap turns it
off and records a withdrawal, and turning it back on shows the sheet again. If the server answers
that a yes is missing, the app shows the sheet instead of an error and sends the request once more
after Turn on.
16- and 17-year-olds are asked once more (C30b). Their prompt now says "What you ask may also
go to TypeSafe's Jev, in the United States" (young-ai-2.1-2026-11). From policy v3.14 a member
whose yes was to the earlier words is asked again, once per phone. Once enforcement is on, the
server counts only a yes to the new words or later.
Retired edge functions (C25, C30a). ai-scheduler (the old meal-plan adoption, last called by
apps from August 2026) and semantic-search-plans (meal search, last called before v1.82.0) both
sent member requests to OpenRouter. Each now answers 410 { code: 'retired' } to every request
before reading a key, a session or the body. Undeploying them is the founder's production step. No
other edge function makes a model call for a member (brief-ledger meters the home brief's spend;
the brief's model calls are agent-server's, behind the gate).
Residual risks accepted. (1) Until the minimum is raised, adults' requests reach Google and TypeSafe on the policy's notice alone, as today. (2) The gate reads up to six small rows per request for an adult once enforced; a read that errors refuses, so a database hiccup can refuse AI until a retry. (3) An Android minimum below the sheet's version keeps the gate inert, by design.
14. 31 October 2026 — a per-account daily spend record
What changes. To cap each account's share of the shared AI spend (§6, §8.7), the database keeps one row per account
per UTC day in inference_account_spend. Each row holds:
- the account's user id;
- the UTC date;
cost_micros: what the account was charged that day, in millionths of a dollar;counted_micros: how much of that counted toward the shared pools;updated_at: the time of the account's last ledger call that day, a check before a request included.
It holds no group, surface, model, provider, token counts or content. An account that uses no AI feature that day has
no row. It is the same kind of usage counter as the weekly token allowance (user_usage).
Who can see it.
record_inference_spendwrites it, and nothing else does.- Only the service role can read it. There is no client grant, and RLS is on with no permissive policy, so a member can't read anyone's row, their own included, or change a cap.
- We read it in the SQL Editor only under the operator access register.
- The daily platform report carries two counts from it, with no ids: how many accounts reached a cap yesterday, and the dollars charged that the pools didn't count.
The single-row inference_account_ceilings table holds only the two limits and holds no personal information.
Retention. A daily job (sweep-inference-account-spend, 02:22 UTC) deletes rows more than 14 days old. The cap reads
the last seven days, so 14 gives a week of margin for a question about a refusal.
Erasure. Rows are removed with the account (on delete cascade from auth.users), at the same moment as the rest
of the account. A test checks this.
Access. The record isn't in the self-serve data export, because that export reads through the member's own client and this table has none. We provide a member's rows on request, along with the rest of what we hold about them.
Children. A child, a managed account and a Device-only group are refused before any ledger call, so no row is written for them.
Review trigger. Revise this section if the record gains a surface, group, model or content column, if its retention lengthens, or if it is ever exposed to a client or included in a report with ids.