← All privacy impact assessments
PIA: Privacy levels — a four-step ladder per group, and Device-only failing closed everywhere
Amended 4 October 2026 — the ladder is now two states. Every group that is not Device-only runs its AI at Vertex
global(cloud-inference §12). The app shows Standard (stored asaustralia,global_records_hereorglobal_all, all identical now) and Device-only; the Australia/Global choice, itsoverseas_processingconsent and the name replacement this assessment describes no longer apply. Device-only and its guarantees are unchanged.
Amended 1 November 2026 — Device-only is being retired (§3E). No group can choose it any more. Groups that have it keep it, and every protection it gives, until one move date 7 days after the notice goes live, when they move to Standard. Their members are told first, on the Privacy screen and on Home. From now on, our servers are never given a copy of a key a group used while it was Device-only. Later the same day: a count found no group at Device-only, so there is no notice, no move date and no move. Every group is Standard, and the privacy policy published with this release (version 3.16) says so (§3E).
Amended 27 October 2026 — one exception to Device-only, on a member's own tap (HQ PIA-007 F2). A member may report one message to GroupApp. The report carries that one message as the reporter's phone shows it, in plaintext, even from a Device-only or encrypted group, with who wrote it and when (member-safety-reports-and-blocks.md). Nothing on our side decrypts or fetches anything for it, and no image is ever sent. The privacy policy's Device-only text adds "…except a message a member chooses to report to us" before the feature reaches members.
Status: Built. The four-step ladder has been live since 9 September 2026, and this document was revised against the code the same day. §6 says which rows are structural and which are conventional, and §8 is the residual risk we accept. Two further additions, §3B (a member's own request ceiling) and §3C (choosing the level while creating a group), were built and merged on 30 September 2026 and have not yet reached any group; see the note under each. On 2 October 2026 we changed who widens the level: an owner or admin now changes it alone and the group is told (§3D). Written: 9 September 2026 · Last revised: 1 November 2026 (Device-only retired: no way in, the notice, one move date and the key ceiling, §3E; later the same day, no group was Device-only, so no notice or move, and policy 3.16 says every group is Standard; 2 October: an owner or admin changes the level without a second member confirming it, and the group is told, §3D; 30 September: a third way into the same choice, while a group is being created, §3C, built but not released; a member's own request ceiling, §3B, built but not released; 29 September: a second way into the same choice, §3A, since built; later the same day, the redesigned daily brief's routes and a correction about where today's brief runs) Nothing here is legal advice. Children's Code citations are to the exposure draft; section numbers may move on registration (due 10 Dec 2026).
The one claim to be careful with. A privacy level is a statement about what our servers can
read and where a model runs, made by a group about itself and rendered back to it as a table.
Every sentence in that table has to be true of the rows it describes on the day it is read. So
this document does not say "we cannot read" as a flat claim: at australia the assistant reads
titles, times and — inside a request — sealed notes; at device_only a title is sealed only once
the history job has reached it; and the dashboard brief is an edge function that runs globally at
every level until it is routed through our assistant server (§8). What this document says is
narrower and dated, and a test suite pins the app's own copy inside those bounds.
Correction, 29 September 2026. The statements in this document that the dashboard brief runs globally at every level (the paragraph above, the §5 row and the first §8 bullet, and the first §9 trigger) date from before the brief moved onto our assistant server on 9 September 2026 (see the cloud inference assessment §8.1). Since then today's brief has run where the platform's default puts every group-data surface, and it reads the level only to refuse a Device-only group. The planned redesign of the brief, which does follow the Global levels, is in §3A and in our home brief assessment.
1. Purpose and scope
A group chooses, once, how much of its information our servers may read and where the models that read it run. Four states, each wider than the last, in one ladder:
| level | keys | server-readable for AI | where models run | assistant / brief / allocation / voice |
|---|---|---|---|---|
device_only |
chat, photos, captions, album names, comments, event notes and places, and task and event titles, sealed under the group key; no kms wrap exists for the current key version. Task notes, baby records and plans are not sealed (corrected 30 September 2026) |
nothing | nowhere | off, failing closed; the on-device brief and the on-device command interface only |
australia (default) |
chat, photos, captions and the assistant transcript sealed on devices; event notes and place readable via the kms wrap inside a request. The same wrapped group key would open chat and photos; only our code keeps it to event notes and places. Baby records, notes included, are not sealed (corrected 30 September 2026) |
titles, times, structure; notes and place inside a request | Sydney, once the Sydney switch is live; a global provider until then, and the screen says which | as today, each behind its consent row |
global_records_here |
as australia |
as australia |
the organising domains — calendar, chore, meal planner, pantry, admin, games — on the global provider with names replaced; the baby domain, the baby summary, voice extraction and the supervisor's extraction stay in Sydney | as today |
global_all |
as australia |
as australia |
every domain global, names replaced, except the supervisor's extraction (it sees the raw sentence and is pinned); where a child is in the group, only a guardian of that child can choose it | as today |
An owner or admin changes the level in one tap, wider or narrower (since 2 October 2026, §3D;
until then a widening took two confirmations). Every applied change writes a
system row in the group chat, and every change that touches overseas processing writes a
consents row (overseas_processing, subject the group — a grant on landing on a Global step, a
withdrawal on leaving them; nothing for a move between the two Australian steps, §4).
A widening lapses after twelve months and falls back to australia through the existing
sweep_expired_consents. Children inherit the group's level, and at every level no under-16
account talks to a model at all — that refusal already exists for child accounts, and this
feature adds no second one.
Out of scope, and covered elsewhere: what the assistant discloses inside a turn at
australia (see the cloud inference assessment); the Tier 2 wrap
mechanism itself (see the Tier 2 encryption assessment); the child-safe variant
(see the child accounts assessment); task notes (a decision on whether
they should be sealed too is still pending — only the task title is sealed here).
Why four and not two. An earlier Standard/Locked pair of settings answered "may our servers
read it" and nothing about "where does the model that reads it run". We asked ourselves whether
Australia could be the default with a plain-language opt-in for overseas models, and whether a
parent could choose a Global option that includes the baby domain — both answered yes, on
conditions tied to the Sydney switch, and the middle step exists so that a group can take the
newer model for its calendar without moving a child's feeds anywhere. There is no custom level:
a matrix of toggles is a way to be insecure by accident.
2. Data involved
| Kind | Whose | Children's? | Sensitive? | Free text? |
|---|---|---|---|---|
groups.privacy_level, privacy_level_changed_at, privacy_level_changed_by |
the group's | no | no — a closed-list word, a date, an id | no |
group_privacy_level_proposals and group_privacy_level_approvals — dormant since 2 October 2026 (§3D): nothing opens a proposal, the open ones were deleted, and the rows that remain are the record of widenings applied before then. The approvals stay owner-private |
the group's | no | no | no |
consents row, purpose overseas_processing |
the group's, given_by the owner or admin who made the change |
no — a household consent, never a child's own | no | no |
group_tasks.sealed_title, group_schedule_events.sealed_title (+ encrypted, key_version, wrapped_data_key on tasks; events already carry them) |
the member who wrote the title | often — "Ada's paediatrician", "Kit's swimming" are ordinary titles | a title can name an appointment; at device_only that is the point of sealing it |
yes |
group_schedule_events.guest_title |
the host's own words, typed for guests | can name a child | no more than a title | yes, plaintext by design — it is what another group is told |
| What reaches a global model at the Global steps | the group's tasks, events, meals, pantry, games, admin (records-here) or everything (all) | at global_all, a baby's counted day and notes |
at global_all, health-adjacent |
yes |
The placeholder map (Member A → user id, Baby 1 → baby id) |
derived per request | no | no | no — held in memory for one request, never stored, never logged |
Not collected, and could have been: who declined a widening (a decline deletes the row); a history of levels (the ledger row and the chat notice are the record); the placeholder map (it exists for one request); a title's plaintext anywhere server-side once the row is sealed; any per-member "consented on" beyond the person recorded as making the change.
Strict necessity (s 38(2)(b)): nothing new is collected about a child. The level is a group
setting, made by one adult owner or admin; the sealed title is the
same title, stored in a form the server cannot read. guest_title is necessary because a locked
host's own title is ciphertext to its guests, and a party with no name is not an invitation.
3. Flows and locations
| Data | Device | Supabase (Sydney) | GCP (australia-southeast1) | Model provider | Region |
|---|---|---|---|---|---|
| The level | read from the groups row the app already loads; the screen renders the table from PRIVACY_LEVEL_MATRIX |
the column; written only by propose_privacy_level and the sweep (a guard trigger refuses a direct client write) |
read PER REQUEST through the caller's RLS-bound client, in our assistant server, never cached | — | Sydney |
| A widening | an owner or admin chooses it, after a sheet saying it applies to everyone at once; the chat notice that tells the other members opens the Privacy screen when tapped | applied at once by propose_privacy_level (since 2 October 2026; until then a second member had to confirm); the consent row and the chat notice, naming who made the change, in the same transaction |
— | — | Sydney |
| A narrowing | one tap by an admin, with a confirmation sheet | applied at once by propose_privacy_level; a withdrawn consent row where it leaves the Global steps; the chat notice |
— | — | Sydney |
Locking (moving INTO device_only) |
the admin device rotates the group key (rotateGroupKey), then runs the title history job for tasks and events; offerServerWrap is not called for the new version |
the existing kms wrap is not deleted (group_key_wraps is append-only, service role included); it wraps a version that is no longer current |
the reader finds no wrap for the current version → every sealed column is "not available", and the route is refused before that anyway | — | — |
A task or event TITLE at device_only |
sealed on the device under a per-row data key, by the code that seals task and event titles; plaintext column holds the fixed placeholder Encrypted; opened once on the way into the store |
ciphertext; rows the history job has not reached stay plaintext | — | — | Sydney |
An assistant turn at device_only |
the app hides the surface | — | buildAgentContext throws PrivacyLevelError before any model call; the voice pairing endpoint, every voice kind, the ceremony facilitator and POST /api/keys/kms-wrap refuse likewise |
nothing | — |
The dashboard brief and the allocator at device_only |
the app renders the deterministic fallback brief / keeps the deterministic allocation | the two edge functions read the level through the caller's client and answer { status: 'locked' } before any model call |
— | nothing | — |
A push at device_only |
— | send-message-notification already carries no text; send-action-notification sends "Something changed in |
— | — | — |
A turn at global_records_here |
— | — | getProvider(node, text, level): calendar/chore/meal_planner/pantry/admin/games → global; supervisor extraction, baby, insight, insight_summary and the voice extraction → Sydney; the health-vocabulary pin still wins; history filtered to non-baby turns; member and baby names replaced with Member A / Baby 1 in the prompt and restored in the reply and in tool arguments |
the organising domains' prompts, names replaced | global |
A turn at global_all |
— | — | every domain global except the supervisor's extraction; names replaced | every domain's prompt, names replaced | global |
The Mail Sifter assessment at global_all (added 21 September 2026) |
— | — | the insight_summary call goes global as above; a SECOND call, a classifier judgment over the same redacted text and the generated options, goes to TypeSafe (Jev), gated on the same isGlobal and the same name map, skipped for health vocabulary, and answered by the Gemini result whenever it does not answer (see the inbound email assessment, § "Jev") |
two global providers: Gemini's, and TypeSafe in the United States | global |
| The residency sentence on the screen | rendered only when /api/usage reports inferenceProvider: 'vertex'; otherwise the interim sentence described in §8 |
— | defaultProvider() |
— | — |
What crosses a boundary and why it is allowed to: at the Global steps, a group's organising
records (or, at global_all, everything) cross to a global provider under a consent an owner or
admin gave on a screen that named the provider's regions in plain words, with names replaced, and
the rest of the group is told in its chat (§3D; before 2 October 2026 it took two adults). Nothing
crosses at device_only. Since 21 September 2026 "a global provider" is two at global_all
for one surface (the Mail Sifter assessment; TypeSafe, United States, no training on Input); the
screen copy now says "our providers' regions", and whether groups that consented under the
singular sentence are notified or re-confirmed is the open owner decision recorded in
the inbound email assessment §8. Since 28 September 2026 the global provider for the assistant
is Google Vertex AI at its global location (Gemini 3.8 Flash), not OpenRouter: the same steps,
the same name replacement, one fewer party between the group and the model (our cloud inference
assessment §9). guest_title crosses to a guest group by the host's own hand, as the
existing mirror carries a title today.
3A. Addition, 29 September 2026: a second way into the same choice
We plan to ask, while a new group is being set up, whether the group should use global AI. The
reason for asking is model quality: the newest models are not yet available in Australia, and
members who would choose them should not have to find a settings screen to do so. The design is
assessed in our Home, widgets and setup assessment §8. This
section records what it changes here, which is a new entry point and nothing else. Built on
29 September 2026, as described below: the setup step and the Settings screen share one piece of
code for the choice, and it calls the same procedure, propose_privacy_level; nothing else in
setup writes the level.
- The same choice, the same procedures. The step offered three answers — global AI for
organising (
global_records_here), global AI including baby-related requests (global_all), or keep the current setting — and nothing is pre-selected. Since 30 September 2026 it offers two: the baby-related Global step is chosen only from the Privacy screen (§3C). Opening the step, skipping it or choosing "Decide later" writes nothing. A choice goes throughpropose_privacy_levelexactly as it does from Settings: the tap by the group's owner or admin applies it (since 2 October 2026, §3D; until then an established group needed a second person), theoverseas_processingconsent row, the chat notice, the twelve-month expiry and the guardian rule atglobal_allare unchanged, and the step writes no column directly. Choosing the baby tracker during setup never choosesglobal_all. - Who sees it. Only an adult owner or admin of a standing group can propose a change from the step. A joiner, an ordinary member and a child account see the group's current level and what it means, read-only, as Settings already shows them; an event group does not see the step; a Device-only group is never invited to leave Device-only by setup — an admin who wants to change it goes to Settings.
- Nothing on the phone is read as the level. Every request is routed by the level the group actually has; no setup record is ever read as the level. (Until 2 October 2026 a choice could wait for a second confirmation while setup continued; nothing waits now, §3D.)
- The words are Settings' words. The level descriptions, the child notices and the residency line come from the same source as the Settings screen, so the two cannot disagree. The step says "for this group", links to the full privacy choices (including Australia and Device-only), and says that changing back affects only future processing. It does not promise better answers, it cites no comparison, and it does not say that choosing a level moves stored records or lets the assistant read anything sealed.
- "Supported requests", because not everything moves. At the Global levels the assistant's steps move to the global provider as §3 sets out. Two things do not, today: the daily brief and the task allocator run on our assistant server in Australia at every level (they read the level only to refuse a Device-only group). The step must not promise a global Home brief or global allocation while that is so. When either moves, this section and the step's words change together. Revised 29 September 2026: the brief is planned to move. Its redesign, assessed and accepted in our home brief assessment, writes one paragraph per group twice a day. At Australia it runs on Gemini 2.5 Flash in Sydney. At either Global level it runs on Gemini 3.8 Flash at the provider's global service with member and baby names replaced — unless its facts carry health vocabulary, a baby as a task's beneficiary or a baby's name, in which case both of its passes run in Sydney, so "Global, records stay here" is not widened to health records. It reads only the level the group actually has, fresh before each pass and before it is stored, and a narrowing deletes what it wrote: every edition when the group moves to Device-only, and every edition written on the global route when the group moves from a Global level back to Australia, including when the group's Global consent lapses. The task allocator still does not move, so the setup step keeps saying "supported requests" and must not promise that every brief goes global.
- A request that carries baby data takes the most restrictive route that applies. The proposed
third stage of the same design adds real conversation and bounded reads across tools. At
global_records_herea combined request that includes baby data is processed in Australia, or its parts are read and answered separately; a request's label is never a way around the Sydney pin on the baby domain.
Nothing in §§5–8 is changed by this addition, and the risk it adds — a member widening the level from a setup screen they are moving through quickly — is answered by the nothing-pre-selected design, "Decide later", the step being offered only to the group's owner or admin, and the chat notice that tells everyone else. (When this was written the two-confirmation rule answered it too; §3D removed that rule.)
3B. Addition, 30 September 2026: a member's own request ceiling
Built and merged 30 September 2026; not yet released. It takes effect once our next release ships, and until then no member can set a ceiling and every request runs exactly as this document otherwise describes. The rest of this section describes the addition as built.
The group's level is one choice for everybody in it. This addition lets one member keep their own requests narrower than the group allows, without asking anybody and without changing the group. The rule members will be told is: the group sets the ceiling; you choose within it. A member of a Global group who sets their ceiling to Australia has what they ask processed in Australia, while another member's request about the group's shared lists is still processed where the group allows.
What is stored. One new value on the member's own profile row: no ceiling (the default, and what every existing member has), or Australia. Nothing sets it on a member's behalf, and there is no other value: a ceiling can only narrow, never widen, so there is nothing to choose beyond "Australia". The member writes it themselves under the same rule that lets them edit their own profile, and nobody else can set or clear it. Like the rest of that row, the value is readable by the other members of the member's groups; it says where that member's own requests run, which is a preference of the same kind as their privacy stance, not a secret about them.
How it is applied. Our assistant server reads the member's ceiling on every request, through the member's own signed-in access and beside the group's level, never from anything the phone sends and never from a cache. The level the request runs at is the lower of the two. Every route that reads the group's level applies it: the assistant (typed and spoken, including the automatic Mail Sifter assessment, which runs inside a member's own assistant request), voice pairing and each voice request, the baby summary, the daily brief, task allocation, the older home brief, goal breakdown, ceremonies, task search and the event time suggestion. An automated check fails any route that reads the group's level without the member's ceiling.
When something cannot be read.
- If the group's level cannot be read, the request is refused, exactly as before. A ceiling never turns a refusal into an answer.
- If the group's level was read but the member's ceiling could not be, the request is narrowed to Australia. Never the reverse.
- Until the new value exists in our database, the server treats the missing value as "no ceiling", which is exactly today's behaviour.
What it cannot change. A Device-only group stays Device-only: the ceiling only narrows. It also does not change what other members' requests do. In a Global group, content a member wrote can still be read by a global model when another member asks about the group's shared lists; the ceiling is about where your requests go, and members will be told that in those words.
Shared results. Two results are stored once per group and read by every member: the baby summary and the daily brief. Each now records the route that wrote it. A member whose ceiling keeps them in Australia is not shown a result written on the global route: the baby summary is written again in Australia for them, and the daily brief falls back to the app's own line for that edition rather than replacing the group's copy. The baby summary's short list of time-of-day cues (read from a baby's notes) is reused as input to the Australian call and is never shown; reusing it sends nothing of that member's anywhere.
Order. This is enforced on the server first. No setting, onboarding step or promise about it appears in the app until the enforcement is live and has been checked, so nothing on a screen can describe a protection before it exists.
Residual risk.
- Old results already stored. A baby summary written on the global route before this change was not marked as such. It is replaced the next time any member opens the tracker, and until then it is not shown to a member with a ceiling unless their own request would be served a stale summary (a spending limit reached, or an empty model reply). No member can hold a ceiling until the setting exists, which is later than this change.
- Other members' requests. As above: the ceiling does not stop a global model reading shared content when somebody else asks. This is the design, and the copy must say so.
- Visible to co-members. Other members of the member's groups can read that the member set a ceiling. Accepted, as for the stance.
3C. Addition, 30 September 2026: choosing the level while creating a group
Built and merged 30 September 2026; not yet released. It takes effect once our next release ships and a creator's app has updated; until then every group is still created exactly as §3 describes today. The rest of this section describes the addition as built.
A person creating a group will be asked two things about the group before it exists: what kind of group it is, and how it should use AI. The second question is a third way into the same four-step choice, under plainer names — Phones only (Device-only), Australia and Global (Global, records stay here). The fourth step, Global, including baby records, is not offered while creating a group, nor on the earlier setup step described in §3A; it is chosen only from the Privacy screen. The kind of group is assessed in our member and group types assessment.
- The kind of group never chooses the level. It suggests one on the next screen, and the suggestion is never wider than Australia, so the highlighted answer can never be the one that needs a consent. A test fails if any kind of group suggests a Global level.
- Phones only and Australia are applied as the group is created. Until now every group started at Australia, so its first phone made the group's key and — at Australia — gave our server a copy wrapped under our key service, before anybody could choose Device-only in setup. A group created at Phones only is now created at that level in the same database step that creates it, so its phones read Device-only before they make the key, and our server is never offered a copy of it. The database's existing rule refusing a server copy for a Device-only group holds besides. The step that creates a group accepts only Australia or Device-only and refuses anything wider.
- Global is never applied at creation. A widening needs the recorded overseas-processing consent
and the note in the group's chat, and both need a group to exist. A creator who chooses Global gets
an Australia group, and the first setup screen then offers the change through the same
propose_privacy_levelprocedure as Settings; the creator's tap on that screen is what asks for it. Arriving at setup, or opening a link to it, proposes nothing on its own. - Where the new way is not yet available. Until the database has the new step, the app creates the group at Australia and applies Device-only straight away through the ordinary narrowing — before the key is made, in the usual case. If that narrowing does not land, the app says so and points to the Privacy screen rather than leaving the creator to believe it applied. A copy of the app that predates this change still creates every group at Australia.
- The example tasks a new group starts with. A new group is seeded with a few example tasks written by the app. In a Device-only group their titles are sealed on the creator's phone as soon as the key exists, by the same job the Privacy screen runs, so every title is sealed from the group's first minute.
- A member's own choice. The same setup now asks each member what kind of member they are. One of the answers, Show less, records the member's own request ceiling (§3B). Nothing on that screen describes the ceiling until its enforcement has been checked; recording it can only narrow where that member's own requests run.
Nothing in §§5–8 changes: who may change the level, the guardian rule, the twelve-month expiry and Device-only failing closed are the same whichever screen the choice was made on. (The two-confirmation rule this paragraph once named was replaced on 2 October 2026, §3D.)
3D. Change, 2 October 2026: an owner or admin changes the level, and the group is told
Built and merged 2 October 2026; not yet released. It takes effect with our next release. The database half reaches every group at that moment, whatever version of the app its members have; the new screens arrive with the app update.
Why. Until now a widening opened a request that a second member had to confirm. In practice that cost a group the feature: in one group the person who created it asked several times, and the other member saw a link in the chat with nothing to confirm, because their phone had an older app that could not draw the confirmation card. A level the group wanted never applied. We decided that the person who already holds the authority to change the group's other privacy-relevant settings — an owner or admin — should be able to change this one too, and that the rest of the group should be told rather than asked.
How it works now.
- An adult owner or admin of a group chooses a level on the Privacy screen (or, for a new group's creator, on the setup step of §3A). A sheet says the change applies straight away for everyone in the group and is announced in the group chat. One tap applies it, wider or narrower.
- The database applies it in one step: the level, who made the change and when, and — where the
change lands on or leaves a Global level — the group's
overseas_processingconsent row, given by that owner or admin and lapsing after twelve months. A notice in the group chat says who set the group to which level; tapping it opens the Privacy screen, which says what the level means. - The guardian rule now concerns who acts. Where a child with a live guardian link is a member, "Global, everything" can be chosen only by an owner or admin who is a guardian of that child. Anyone else is refused with a sentence saying so. A guardian who is not an owner or admin cannot choose it either; an owner can make them an admin. "Global, records stay here" is unaffected, because it keeps a baby's records in Australia.
- Requests waiting for a second confirmation when this shipped were deleted, and confirming one now changes nothing. The old messages stay in the chat, drawn as a closed request with a link to the Privacy screen.
What did not change. No member who is not an owner or admin, and no child account, can change the level; no app can write the level directly; Global is never applied when a group is created or from a link; narrowing is one tap; every change a person makes is announced; a Global level lapses after twelve months and falls back to Australia; and Device-only fails closed everywhere.
Residual risk, accepted.
- One person decides for the group. An owner or admin can send the group's organising records — or, with the guardian rule satisfied, everything — to a global model without anybody else agreeing first. Other members learn of it afterwards. We accept this because the level is a group setting like the others an owner or admin already decides for the group, each of which is announced; the change is named in the chat with who made it; any owner or admin can narrow it again in one tap; it lapses after twelve months; names are replaced before anything is sent; and a child's records need that child's guardian.
- Being told is not being asked, and the notice is quiet. The chat notice does not send a notification to anybody's phone, does not mark the chat unread and does not count towards a badge, the same as every other notice of a setting change. A member finds out when they next open the chat or the Privacy screen. A notification to every member for this change would be a further decision.
- A member who disagrees and is not an admin cannot reverse it. They can raise it with the group. Once the request ceiling of §3B is released and checked, a member will also be able to keep their own requests in Australia whatever the group chooses.
- A guardian who is not an admin. In a group whose children's guardians are not owners or admins, nobody can choose "Global, everything" until an owner makes one of them an admin. This is the guardian rule doing its job.
Structural or conventional. That only an adult owner or admin changes the level, that the child rule names who acts, that the change, the consent row and the chat notice land together, and that a leftover request can no longer apply anything are all enforced by the database (structural). The sheet's wording and the notice opening the Privacy screen are app code (conventional, test-pinned).
3E. Change, 1 November 2026: Device-only retired (tell, then move)
Amended later on 1 November 2026: no group was Device-only. We counted the groups in production and found none at Device-only. So nothing is told and nothing moves: the move date is never set, so the Privacy screen and Home show no notice, and no group's chat gets a message. What stays is everything that keeps a group out of Device-only and the rule that our servers never get a copy of a key a group used while it was Device-only. The privacy policy published with this release (version 3.16) says every group is Standard, keeps one line of Device-only's history, and no longer offers it as a choice; we skip version 3.15, so members acknowledge one edition. We count again on release day, just before the release and just after its database change. If either count finds a group, the plan below runs as written, with a dated notice, and the policy is corrected.
Built; not yet released. This is the first of several changes. This one stops new Device-only
groups, tells the members of existing ones, and closes a gap in how a group's key leaves Device-only.
No group's level changes in it. A later change moves the remaining groups to Standard on the move
date, and posts a notice in each group's chat when it does. Security's assessment of the whole
retirement (HQ addendum 2026-11-01-device-only-retired, conditions R1 to R15) is the record of the
decision; this section says what the app does.
Why. We decided to remove Device-only from the app. Its users were few, it kept a second
set of features (an assistant on the phone, a brief nobody gets any more) that the rest of the app
does not need, and no remaining level gives a group "no AI at all": every Standard group's AI runs on
Google's service at its global location. Nothing replaces Device-only. The policy will say plainly
that every group uses AI.
What changes now.
- No way in. An owner or admin can't choose Device-only, a new group can only start at Standard, and the database refuses any write that would move a group into Device-only, from any writer. The one exception is an event group set up by a group that still has Device-only: it takes its parent's level, as event groups always have, so it keeps failing closed like its parent.
- The notice. Every member of a Device-only group, child accounts and members who join during the notice included, sees on the Privacy screen that the group moves to Standard on the move date, what that means (AI features, Google's AI service, any country), and that an owner or admin can move it sooner. Home shows the same news with a "See what changes" button until it is dismissed on that phone. Walls don't show it. A child account's wording names the adults in the group, and never says the assistant will work for the child. The Privacy screen also says what Standard means for a child's records, in every Device-only group whoever is in it, so no group with a child can miss it. An adult owner or admin can move the group to Standard straight away, through the same confirmation and chat announcement as §3D.
- One move date for every group. It is the Sydney date of the release that makes this live (the app update published and the new policy edition in force) plus 8 days, so every member gets at least 7 full days of notice. It is held in our database, set at release, and read by the notice and the policy. It isn't counted per group from when someone first saw the notice: that would need a record of who opened the app, would let one member's visit start the clock for members who saw nothing, and would make the policy's date untrue. Until it is set the app shows nothing; once it has arrived, the notice says the group is about to move rather than naming a day that has passed.
- A key used while Device-only never reaches our servers. Until now, leaving Device-only did not change the group's key. Once the group was Standard, an owner's or admin's phone could give our server a copy of the key it had used while Device-only, and with it what was sealed under that key during that time. Now the database records the key version current at the moment a group leaves Device-only, and refuses a server copy of that version or any earlier one, for good. An owner's or admin's phone makes a new key first and offers only that. Nothing moves content sealed under the older versions onto a key our server holds, so it stays sealed: "What was encrypted on your phones stays encrypted" is true.
What does not change yet. Every server, notification, voice and key check still treats a group that has Device-only as Device-only until it moves. No member is asked for consent and no consent row is written: this is the §3D model (an admin decides, the group is told) with notice before the change and an announcement at it, because here nobody in the group chose the change.
A correction to §7. §7 says that at Device-only "a child in the group has the strongest position this app can offer: nothing they or their parents write about them is readable by our servers". That was not true from the app update released after 17 October 2026, when every group's everyday records became ordinary text in our database at every level, Device-only included (content-encryption-v3.md). What Device-only gave a child until now was that nothing about them reached an AI model, and chat and photos were sealed. After the move, records about a child can be read by the assistant and the daily brief an adult uses, as in every Standard group. Under-16s still have no assistant and no notification text, and 16- and 17-year-olds use AI only after their own yes, so nothing a child does starts a model call.
Residual risk, accepted.
- Notice, not consent. An adult who objects can't keep the group's records from AI except by asking an admin, leaving, or deleting content. The per-adult AI switch is built but not enforced yet.
- Late learners. A member who doesn't open the app during the 7 days, or whose app can't take the update, learns of the move from the chat notice after it.
- Groups that left before this. A group that left Device-only before this change may already have given our server a copy of a key it used while Device-only. Which version it was can't be worked out reliably now. Those keys are only opened inside a member's own request.
- Children's records reach AI in moved groups without a guardian's say, as in every Standard group. Guardians are told by the notice.
- No group-wide "no text in notifications". Device-only was the only way to get that; message previews stay a per-member setting.
Structural or conventional. That no writer can move a group into Device-only, that the move date is one value no app can write, and that a server copy of a Device-only-era key is refused are enforced by the database (structural). That the phone makes a new key before offering a copy, and that no job moves sealed content onto a server-held key, are app code (conventional, test-pinned), backed by the database refusal. The notice's wording and who sees it are app code (conventional, test-pinned).
4. Legal basis and consents
overseas_processing(a consent purpose, subject the group) — landing on a Global step writesgrantedwithgiven_bythe owner or admin who made the change andgiven_by_kind = 'group_admin', from insidepropose_privacy_levelas the change applies — until 2 October 2026, from insideapprove_privacy_levelas the second confirmation landed. A narrowing fromglobal_alltoglobal_records_hereis a re-consent to less, by an admin, on a screen, and writes a fresh grant; leaving the Global steps writeswithdrawn. A move betweenaustraliaanddevice_onlywrites no row — there is no overseas processing to consent to or withdraw, and awithdrawnrow for a purpose never granted would be a dated record of nothing. The row is written by the database procedure itself and not directly by the app, because the level applied and the consent recorded must be one transaction. It expires at twelve months (ss 15–16);sweep_expired_consentsthen appendsexpiredand sets the level back toaustraliaunder theapp.privacy_level_writearm, and the settings row renders the expiry prompt (our rule that the app says so when it alters a value the group supplied). A group ataustraliaordevice_onlyhas no currentoverseas_processinggrant, which is correct: there is nothing overseas to consent to.- Unbundled (s 14(3)(b)). The level is one choice on one screen with one sentence per step;
it is not a stance, not a preset, and nothing writes a
grantedrow that an owner or admin did not ask for on the Privacy screen or the setup step. The app's stance definitions exclude every consent-class entry, andgroups.privacy_levelis consent-class, so a stance cannot touch it. - A guardian makes the change at
global_all(PRV03). Where anychild_guardianslink is live in the group (is_under_18(child)),propose_privacy_levelrefusesglobal_allunless the owner or admin making the change is a guardian on such a link (until 2 October 2026: unless one of the confirmers was). The reasoning is §7. A child account cannot change the level at all (the RPC refuses an under-16 caller). - Voice grants — the restrictive INSERT policy on
voice_grantskeyed onprivacy_level <> 'device_only'means a locked group cannot pair a device, and a grant paired before locking is refused per turn by the server's re-read. - Withdrawal (s 17) is one tap: narrowing never needs a second person.
5. Risks and mitigations
| Risk | Who is harmed | Mitigation | Structural or conventional? | Residual |
|---|---|---|---|---|
A member sets the level wider than the group agreed, or a client PATCHes privacy_level straight to global_all |
every member; every child in the group | enforce_privacy_level_admin_only refuses any write to the three columns unless the app.privacy_level_write arm is set, and only the RPC and the sweep set it; every change is applied inside propose_privacy_level, for an adult owner or admin only, never by a client writing the column |
structural | any one owner or admin can widen, and the group is told afterwards (§3D); that is the design |
| An owner or admin widens the level without the rest of the group agreeing (§3D) | every member | only an adult owner or admin can; the chat notice naming who made the change is written in the same transaction; any owner or admin narrows in one tap; the Global consent lapses after twelve months; a leftover confirmation request cannot apply anything | structural as to who may change it and the notice being written; conventional as to members reading the chat | members learn after the fact, and the notice does not notify a phone |
| A child's feeds, sleeps, medicines and notes go to a global provider on the say-so of somebody who is not the child's parent | the child | PRV03: at global_all in a group with a live guardian link, the owner or admin making the change must be a guardian; the screen's child notice says in plain words what crosses at each Global step |
structural as to the guardian rule (the RPC); conventional as to the notice (copy, test-pinned) | a guardian of one child makes the change for a group holding two children of different guardians — see §8 |
The group locks, but the server still holds a kms wrap and can read sealed notes |
the group | locking rotates the group key; the reader looks up the wrap for the row's version and finds none for the new one; offerServerWrap reads the level and does not call the wrap route; the wrap route refuses device_only |
structural as to "no wrap for the current version" (the rotation is a real key change; group_key_wraps is append-only so nothing can quietly re-add one without the route, and the route refuses); conventional as to offerServerWrap's read (client code) |
rows written under the OLD version keep a wrap the server could still open if a route ever read them — and every AI route is refused for a locked group before it reads anything (below). Stated in §8 |
| A locked group's titles are read by the assistant, the brief, the allocator or a push | the group; a child named in a title | buildAgentContext throws PrivacyLevelError before any model call; the voice pairing endpoint, every voice kind and the ceremony facilitator refuse; the two edge functions answer locked; an automated check fails any server route that does not consult the level |
conventional, test-pinned (source scans and fakes in npm run check) — the database does not refuse a SELECT to the caller's own client, because the caller is a member |
a title the history job has not reached is plaintext at rest; the generated tools' select('*') would return the placeholder if a route were not refused first |
The dashboard brief runs on a global provider at every level, including australia |
the group | none — it is an edge function with no runtime identity (the same residual the cloud inference assessment records at its §8.1) | — | accepted, §8: the screen's australia sentence is an interim one until the brief is routed through our assistant server |
| A name reaches a global provider at a Global step | a member; a child | replaceNames maps every member and baby name (longest first, so a name that is a substring of another is not split) to Member A / Baby 1 in the intent text, the recent turns and the roster in the prompt; restoreNames un-maps the reply and every string argument of every tool call before a write; the check fails a global-eligible node with no replacement in its path |
conventional, test-pinned (pure functions with a round-trip test; a source scan) | a name typed in a form the map does not hold — a nickname, a misspelling, a possessive — is not replaced; a name inside a title read back from the database by a tool result is replaced only if it matches the roster |
A baby-domain turn is routed globally at global_records_here |
a child | SYDNEY_PINNED_NODES still wins in getProvider; filterHistoryByOrigin withholds baby turns from a global step; an automated check asserts no baby node can resolve global at that level |
conventional, test-pinned | an untagged history turn (from before the Sydney switch, or after a restart) is admitted — the same residual the cloud inference assessment already records at its §8.5 |
| The consent lapses and the group keeps running on the global provider | the group | sweep_expired_consents appends expired and sets the level to australia in one savepoint; a test asserts it |
structural (the sweep is a cron job in Postgres) | a night's delay, at most |
A guest group's mirror of a locked host's event shows Encrypted as its name |
the guests | guest_title, typed by the host, propagated into the mirror's title by propagate_event_to_mirrors; the app asks for it when a locked group hosts |
structural as to propagation (trigger); conventional as to the app asking | a host that never types one leaves its guests with the placeholder |
| The "what we can see" table drifts from what the server does | the group (misled) | the table is one matrix in the app's own code, rendered verbatim; the routing map in our assistant server is keyed by the same level ids; a copy test scans every cell for overclaims | conventional, test-pinned | the two are in different packages and agree by test rather than by import — see §8 |
| An under-16 account changes the level | the group | propose_privacy_level refuses an under-16 caller (PRV05), and approve_privacy_level now applies nothing for anybody; the screen is read-only for a child, using the same "decided for you" treatment used on a child's own account screen |
structural (RPC) and conventional (screen) | — |
6. Structural versus conventional
Structural
- The three columns are unwritable by any client:
enforce_privacy_level_admin_only(BEFORE UPDATE OF the three columns, and BEFORE INSERT for a non-default level) refuses without the transaction-localapp.privacy_level_writearm. - Every change applies inside
propose_privacy_level, for an adult owner or admin of a standing group only (since 2 October 2026; until then a widening applied insideapprove_privacy_levelwhen the approvals reached the stored count). approve_privacy_levelapplies nothing; a confirmation request left over from before cannot change the level.PRV03, the guardian rule atglobal_all, is a predicate in the RPC overchild_guardiansandis_under_18, applied to the person making the change.- The consent row and the chat notice are written in the same transaction as the level.
- The sweep's fallback to
australiais a Postgres cron job, armed. - A locked group cannot pair a voice device (restrictive INSERT policy on
voice_grants). - Locking rotates the key;
group_key_wrapsis append-only, so the old wrap is inert for the current version and no client can add a new one except through the route, which refuses. - A
kmswrap for adevice_onlygroup is refused by a restrictive INSERT policy ongroup_key_wrapskeyed on the group's level — so even a route that forgot would be refused by the database. - A member's request ceiling (§3B) is a closed list in the database (none, or Australia), and only the member can write their own.
Conventional, test-pinned
Every route in our assistant server consulting the level before a model call, enforced by an automated check;
PrivacyLevelErrormirrorsEventGroupError's three-place pin used elsewhere in that server.The level → provider map, the Sydney pins at the Global steps, the history filter (an automated check, extended for this feature).
Name replacement in the prompt and restoration in the reply and tool arguments (a round-trip test; the check's source scan).
The two edge functions answering
locked(Deno; asserted by a source scan in the unit suite, not executed).The push bodies (a test reads the
pg_netqueue in a rolled-back transaction, the same pattern used elsewhere for notification tests).The copy: every cell of the matrix, the tooltip's rules, the child notices, and the sheet before a change saying it applies to everyone (a dedicated test).
The chat notice of a level change opening the Privacy screen when tapped.
offerServerWrapnot being called for a locked group (client code).The title history job running when a group locks (client code;
countUnsealedTitlesis the truth the Privacy screen renders).Every route applying the member's request ceiling beside the group's level, the failure rules (unreadable group: refused; unreadable ceiling: Australia; value not yet in the database: no ceiling), and the two shared results not being shown across routes (an automated check).
Conventional, not pinned
- The host typing a
guest_titleat all.
7. Best interests of the child
- What it gives a child. (Corrected 1 November 2026, §3E: since the app update released after
17 October 2026 a child's everyday records are ordinary text at every level, so the first sentence
below is no longer true; Device-only kept them away from AI models, not from our servers.) At
device_onlya child in the group has the strongest position this app can offer: nothing they or their parents write about them is readable by our servers, and the on-device command interface is the only assistant the group could ever have — a design principle set out in our privacy posture. Ataustralia(the default) a child's records are processed in the country whose regulator can be asked about them. - What it costs them. At
global_all, a baby's counted day and a parent's notes about them may be processed in any of the provider's regions. The child cannot consent to that and will not remember it. The mitigations are that names are replaced, nothing is retained or used for training (confirmed against the provider's own terms), the choice expires in a year, and — the reasoning the rule rests on — the person who makes the change must be somebody with responsibility for that child, not an admin who happens to share a calendar with them. That isPRV03: atglobal_all, in a group with a live guardian link, the owner or admin making the change is a guardian (since 2 October 2026; before, a guardian had to be among the confirmers). The Code's s 10 asks whether the handling is in the child's best interests; a guardian's judgement that a newer model's help with the household's day outweighs overseas processing of a feed log is a judgement a guardian is entitled to make and a stranger is not. - The middle step is the safer design for most groups, and it is why the ladder has four rungs
rather than three.
global_records_heremoves the calendar and the chores to the newer model and leaves the baby domain, the baby summary and voice logging in Sydney. A parent who wants the capability without moving the child gets exactly that. - Whose interest won where they diverge. The child's, twice: the default is
australiaand not a Global step; and the guardian rule is a floor a group cannot argue itself past. The parent's, once:global_allexists at all, because we asked ourselves whether a parent could choose a full Global option including the baby domain, and answered yes on conditions, and a parent who has read the plain-words notice about feeds, sleeps, medicines and notes is the person the Code says may decide. - What the child is told (s 33). The child's own account page already says that nothing they do reaches a model. This feature adds no surface for children; under-16 accounts see the level screen read-only with the line "this one is settled by your group's adults".
8. Residual risk we accept
- The dashboard brief is global at every level, including
australia. It is an edge function with no runtime identity and cannot reach Vertex (the same residual the cloud inference assessment records at its §8.1). Theaustraliasentence on the screen is therefore an interim one ("processed by a service that is not in Australia yet — we will say so here when it moves") until the brief is routed through our assistant server, and the residency sentence renders only when/api/usagereportsvertex. Atdevice_onlythe brief is off, so the residual does not reach a locked group. - A locked group's titles are sealed only after the history job finishes, and the job runs on an admin's device. Until then a title written before the lock is plaintext at rest, and the Privacy screen shows the count. Dated in the copy. Since 10 October 2026 the Devices screen's calendar card says, in a Device-only group, that event titles are encrypted too, and one tap away that older titles are finished in the background with the count on the Privacy screen; before that it showed every group the Standard sentence, "titles stay readable".
- Rows written under the old key version keep a
kmswrap the server could open if a route read them. No route does — every AI route is refused for a locked group before it reads a row — but the property is "no reader", not "no key". A future route added without that safeguard would be refused by the same automated check; a route added with the check deliberately bypassed would find the old wrap. Accepted, because the alternative (deleting the wrap) breaks the append-only property that makes the wrap table trustworthy at all. - The guardian rule is "a guardian", not "every child's guardian". A group holding two
children of different guardians widens to
global_allon one guardian's change. The alternative — every guardian must agree — turns one person's act into an n-person ceremony with no bound, and the Code does not ask for it. Accepted; the chat notice names who made the change. - One owner or admin can widen the level alone (since 2 October 2026). §3D sets out why, and what the group is told.
- Names are replaced by string match against the roster. A nickname, a misspelling or a
possessive form is not replaced. The pin (Sydney for the baby domain at
global_records_here) is what protects the records that matter most; the replacement is defence in depth for the organising domains. - The Global steps are exercised by unit tests only until the Sydney switch is live and a group has chosen one on a real device. Nothing here has met a phone, Vertex, or KMS.
- The matrix on the screen and the routing map on the server are two separate pieces of code that agree by test rather than by shared code (a build setting keeps the assistant server from importing the app's own code directly). A change to one without the other fails a test, not a build.
guest_titleis the host's choice. A locked host that never types one leaves its guests withEncryptedas the party's name. The app asks; it does not require.
9. Review trigger
- The dashboard brief is routed through our assistant server (the
australiaresidency sentence becomes unconditional — and the residual above closes). - The
INFERENCE_PROVIDER_DEFAULTsetting is flipped tovertexin production. - Task notes are sealed — the
device_onlyrow of the matrix gains a column. - A fifth level, or a
customone, is proposed. (The answer should be no.) - A global provider other than Gemini's is proposed for any node at
global_records_here, or for any surface beyond the Mail Sifter assessment atglobal_all— the first (TypeSafe, 2026-09-21) was assessed in the inbound email assessment; a second is this document's own change. - Newer models arrive in Sydney — the tooltip says this choice "may go away, and you will be told"; that sentence has to be honoured.
- A second guardian rule is asked for (every guardian, or the child's own assent at 15+).
- A privacy-level change is to send a notification to members' phones, or a second confirmation is to come back (§3D).
- A setting or any copy that offers the member's request ceiling (§3B) — it is reviewed against the enforcement then, and it must say that other members' requests are not affected.
- A second ceiling value, or a ceiling that could widen (the answer to the second should be no).
10. Checklist against exposure draft s 38(2) (verified 2026-09-08)
| s 38(2) | Requires | Answered in |
|---|---|---|
| (a) | the nature, scope, context, flow and purposes of the handling of children's personal information | §§1–3 |
| (b) | why the collection of children's personal information is strictly necessary to provide the service | §2 |
| (c) | how the collection is done by lawful and fair means | §4 |
| (d) | whether the handling is consistent with the best interests of the child, with the reasoning | §7 |
| (e) | specific information about how the entity complies with this Code and the Act | §4 and §6 |
| (f) | the risk of harm to, and potential impact on, children — physical, emotional, developmental or material | §5 |
s 38(3): conducted before the feature is made available — this assessment was written before the underlying database change went live. s 39: listed on the public register.
11. Sources
- Children's Online Privacy Code, exposure draft —
docs/sources/(ss 10, 13, 14(3)(b), 15–17, 33, 38–39). docs/pia/cloud-inference.md(the provider, the region, the pins, §8.1 the edge-function residual, §8.5 untagged history).docs/pia/tier2-kms.md(the wrap, the reader, the audit row).docs/pia/child-accounts.md(the guardian invariant,is_under_18,child_guardians).- In repo:
supabase/migrations/<version>_ws16_privacy_levels.sql;supabase/migrations/<version>_privacy_level_admin_applies.sql;lib/privacyLevels.ts;agent-server/src/privacyLevel.ts;agent-server/checks/privacyLevel.check.ts;agent-server/checks/regionRouting.check.ts;__tests__/privacy_levels_rls.test.ts;lib/__tests__/privacyLevels.test.ts;lib/__tests__/privacyLevelsCopy.test.ts;docs/encryption_plan.md§4 and §11;docs/privacy_posture.md§3 and §7. - Our own design and planning notes made across earlier drafts of this feature.