← All privacy impact assessments

PIA: the baby tracker

Amended 30 October 2026: deleting a group. An owner can delete a group. Members are told at once, and the group is deleted seven days later unless the group's owner cancels; meanwhile anyone can export their own data or leave. Deleting removes everything the group holds for everyone in it, children included: messages, photos (sealed or not), lists, routines, baby records, the group's inbox and its attachments, and its keys. Our backups expire within 90 days and a deleted group is not restored from them. A star rating a member gave stays, with no group attached. Copies of the group's events in connected groups say the group no longer exists.

Status: This is a retrospective assessment, written against the code as it stood on 7 September 2026, after we removed the live countdown timers from feed tracking. The feature had already shipped by the time we wrote this; that is what "retrospective" means. Its value is the baseline it sets for the next change to this part of the app. One planned change — a deliberate way to correct how a feed's parts are grouped — was assessed on 18 September 2026, ahead of being built; see §3.3. A second — the growth history, its figure, its CSV, and the assistant's growth card and figure — was assessed on 21 September 2026, ahead of being built; see §3.4. A third — sealing the tracker's own words on the phone — was built on 30 September 2026 against this assessment (§3.5); it has not reached any phone yet and will do so with our next database release and the app update that follows it. Written: 7 September 2026 · Last revised: 30 October 2026 (deleting a group); 20 October 2026 Nothing here is legal advice.

As built, 30 September 2026. The free text a household writes in the tracker — a log's note, a medicine's name, a food, what an immunisation was, a baby's profile note, a handover note, a book's title, a care note's medicine, and the tracker's own summary sentence — will be sealed on the phones that write it, as §3.5 describes. Times, types, amounts, doses, temperatures, growth measurements and a baby's name will stay readable, unchanged. This has been built and merged but not released: it takes effect only once our next database release ships and a member's phone has updated to the app version that writes and reads the sealed form. Until then, and for every record written before that point, the words stay exactly as §3 below describes them today.

Changed 20 October 2026: the tracker's line is written on the phone. The sentence at the top of the tracker card used to be written by a model on our assistant server, from the digest §3.1 describes. From the app update after this change it is written by the app itself, on the phone, from the entries already there: the last few things logged, anything less common from the same day, and the latest note in the parent's own words. Nothing about the baby leaves the phone for it, and it says nothing about whether anything is normal. Phones still running an older version keep using the server until they update. The switch that turns the line on is unchanged for now. It still also gates the home screen's insight and brief, which do send a short count of the day to a model. Its wording, which still describes the old way of writing the line, is being revised.

1. Purpose and scope

A parent, usually exhausted, records what an infant did — fed, slept, nappy, medicine, temperature, a note — so that the other people caring for that infant know, and so that they themselves know at 3am what happened at 11pm. That is the whole purpose. It is the app's most sensitive surface and the one whose design has been cut back hardest.

In scope: group_babies, baby_logs, baby_feed_sessions, baby_feed_due_acceptances, baby_care_guidance, the baby books tables, baby photos, the deletion machinery, and every path by which any of it reaches a model.

Out of scope: whether the app is a "health service" under the Privacy Act. That question is open — we have deliberately left it open until the app has been tested with families we know. Nothing in this document waits on it: the app is built to the Children's Code either way, and the answer only decides whether the small-business exemption has already been lost.

The standing position, which is a positioning decision and not an accident: this is an organisational tool and it holds no clinical content at all — no schedules, no thresholds, no intervals, no verdicts. It got there by deletion, three times: the immunisation milestone schedule (2026-08-28), the six app-supplied medicine and fever guidance values (2026-09-02), and the entire evaluation layer that reached conclusions from the household's own numbers (2026-09-02). The third is the one worth understanding, because it shows the first two were about authority, not about numbers.

2. Data involved

Kind Whose Children's? Sensitive? Free text?
group_babies — name, birth date, photo, feeding preferences, display settings, a free-text bio (notes) an infant's yes plausibly health information yes — the bio
baby_logs — log_type, started_at, ended_at, notes, details jsonb, logged_by_user_id, logged_via an infant's; attributed to the carer yes yes, on any reading yes — notes, and free-text details fields
Log types recorded: feed, sleep, nappy, note, medicine, temperature, bath, nasal_aspiration, vomit, tummy_time, growth, solids, immunisation medicine (name, dose, unit), temperature (°C, site), growth (weight, length, head), immunisation (free text) are the sharpest
baby_feed_sessions — a sitting: started_at, ended_at, sealed_by an infant's yes yes no
baby_feed_due_acceptances — the action ref, proposed and accepted occurrence times/details, resulting sitting/component ids, accepting/undoing member and timestamps an infant's; attributed to the carers who accepted or undid it yes yes no
baby_care_guidance — what the household says the limits are: kind, subject, min_interval_minutes, max_doses_per_day, threshold_celsius, source, who confirmed it, when recorded by the household, sourced from their own professional yes yes yes — subject and source
Baby photos an infant's yes no —
baby_history_deletions — deletion request rows the group's no no no

What is deliberately not collected, which is the more interesting list:

The one thing the app does say, and says unconditionally. A constant called DISCUSS_WITH_A_PROFESSIONAL, rendered verbatim in every medicine and temperature notice with no branch able to suppress it:

"This app keeps the record; it does not check it. Take these numbers to your doctor or nurse."

The referral is unconditional because silence is a verdict. Referring only when something looks worth referring is a conclusion delivered by omission: a parent told nothing concludes there is nothing to be told. A test pins its presence across a matrix of readings deliberately including the unremarkable ones, and scans every notice this part of the app can produce for verdict vocabulary — normal, fine, safe, high, low, should, recommend, too soon, straight away. Both blocks are mutation-tested.

Dormant, unread schema. medical_milestones, baby_milestone_events and four group_babies columns (track_immunisations, is_aboriginal_or_torres_strait_islander, has_medical_risk_condition, jurisdiction) survive the immunisation retirement because migrations are additive-only. They are unread by any code path, RLS-guarded, and covered by a blocklist that refuses them to the assistant (REFUSED_COLUMNS). They are still personal information about a child sitting in a table, and the honest statement is that they hold whatever was written before the retirement and nothing since. Dropping them is on our list of planned cleanup work.

3. Flows and locations

Data Device Supabase (Sydney, ap-southeast-2) Model provider
Every log, session, guidance row written and read on the carers' devices stored in plaintext, RLS-scoped to the group only by the three paths below
Baby photos picked, resized and re-encoded on device private avatars bucket, avatars/babies/<group_id>/<baby_id>/<ts>.jpg, served by signed URL no
Deletion requests minted on device with a client-side id, persisted in an outbox until acknowledged baby_history_deletions, re-applied nightly for ever no

Revised 30 September 2026: we have built, but not yet released, sealing the words in these records on the phones — see §3.5. The paragraph below describes the tracker as it runs today, before that release, and will keep describing every record written before a phone starts sealing them.

Nothing here is encrypted at rest. Encryption code exists elsewhere in the app and is unit-tested but is not wired to this feature; the only ciphertext column in the whole schema is review_moderation_log, which has nothing to do with babies. An operator holding the Supabase dashboard or the service-role key can read any infant's logs. That is the app's known state, it is stated in the privacy policy, and it is what our planned work to encrypt data at rest exists to change — baby notes are on the priority (Tier 1) list for that.

3.1 The three paths to a model, and the gate that governs two of them

group_babies.show_recent_summary (default false, added 17 August 2026) is ONE consent, and every ambient surface that sends baby logs to a model inherits it. The check fails closed: unless the stored value is explicitly true, nothing is sent.

Path What travels Gated by show_recent_summary?
Tracker summary card → a summary endpoint on our assistant server. Older app versions only since 20 October 2026; the current app writes the line on the phone and sends nothing for it An allowlisted digest of the day's log types and counts, built on our assistant server: only the detail keys allowed per log type (feed: source, side, volume; nappy: status; vomit: kind; sleep/tummy_time/bath/nasal_aspiration: none; note: the free text), plus the baby's bio capped at 500 characters, plus clock-anchored cues. Model: a fast, low-cost hosted model (google/gemini-3.5-flash-lite). The allowlist is the boundary. Yes, and re-checked on our server. Our assistant server fetches the row itself and returns a 403 error if it is false, rather than trusting the client.
Home dashboard insight Deterministic count and clock lines only — "last feed 2:15 pm, 5 feeds today, 3 nappies today, 1h 20m of sleep logged today". Never raw logs, never free text. Yes, plus a second gate: the baby must be on that user's dashboard.
The assistant's explicit baby reads — two tools the assistant can call directly, get_recent_baby_logs and query_baby_stats get_recent_baby_logs returns up to 50 rows: id, log_type, started_at, ended_at, nappy_status, fluid_volume_ml, details, notes — including the free-text notes, fenced as untrusted. query_baby_stats returns daily aggregates. No. These deliberately bypass the gate, and this is documented in terms.

That third row needs stating plainly, because it is the most surprising fact in this document and it is easy to describe the app wrongly around it. The reasoning recorded for that decision reads:

"EXPLICIT CONSENT: The user explicitly asking the private assistant about their baby is a different consent basis than the ambient auto-summary on the home screen. Therefore, this tool (and query_baby_stats) deliberately DO NOT check group_babies.show_recent_summary."

The reasoning is defensible: an ambient summary generated because a screen was opened is a different act from a parent typing "how many nappies today?". It is not a bug and this assessment does not recommend changing it. But two things follow that a reader would otherwise get wrong:

  1. No copy anywhere may say that turning show_recent_summary off stops a baby's data reaching a model. It stops the two ambient surfaces. A parent who asks the assistant directly still sends logs, including free-text notes. Any privacy tier promising "no baby data leaves the device" is false however the switches sit.
  2. A planned consent ledger should record the explicit-ask basis as its own thing, rather than folding it under baby_summary_to_model. Two different bases recorded as one purpose is a record that cannot answer the question it exists to answer.

Region. All three model paths run wherever the assistant currently runs, which today is OpenRouter → Gemini with no Australian processing — the router decrypts in the United States. We plan to move group data, including every baby surface, to Google's Vertex platform in its Australian region (australia-southeast1). Until that happens, an infant's feed log crosses a border when a parent asks about it, and the cross-border notice must say so (exposure draft s 26).

3.2 Voice never writes a baby record

As of 5 September 2026, a spoken baby request becomes a BABY_LOG_PROPOSAL card in action_stacks (status: 'pending') and writes nothing until a person confirms it on screen — strictly stronger than a spoken "yes" heard once by a room, because the values are visible and editable on the card. Proposable kinds: feed_log, feed_end (legacy), nappy, sleep, note, medicine. Refused by name in a blocklist called REFUSED_BABY_LOG_KINDS: immunisation (a model must not normalise that free text into a vocabulary), care_guidance (the household's own recorded limits are not a model's to touch), and temperature and growth (a clinical reading and a measurement nobody actually took).

Revised 21 September 2026 (§3.4): growth leaves the refused list for the typed assistant only, because the card is exactly the safeguard the refusal reason asked for — the number is on screen, in the parent's own units, before it is a row. The spoken door still refuses it: it has no screen to show the number back on, and "6.2" and "6.7" are one misheard syllable apart.

Revised 9 October 2026: the spoken door now raises the same growth card. What the refusal protected against — a misheard number becoming a record — is answered by the card, not by the voice: nothing is written until a member has read the number on screen, in kilograms and centimetres, and confirmed it, and the spoken reply says only that a card is waiting, never the number. No new data reaches anyone: the spoken sentence already went to the voice extraction model, which used to answer "not understood" for growth; it now fills three number fields instead. The card's numbers sit in action_stacks like a typed growth card's — plaintext, household-visible, the same as the row they become. Structural: the voice door has no path that writes a growth row; the server refuses a number outside the tracker's bounds and the app's resolver checks again on confirm. Residual, accepted: a member can confirm a misheard number without reading it, which they can equally do by mistyping one.

3.3 Planned: correcting how a feed's parts are grouped (assessed 18 September 2026, not built)

The app already joins a nurse and the top-up bottle that follows it into one sitting automatically. When that join gets it wrong — two parts of one feed left separate, or two separate feeds merged — a carer today has no honest remedy short of deleting and retyping, which is a worse record. The planned change adds an explicit correction: combine an entry into a nearby sitting, move it out, or split a sitting, previewed before it is confirmed, applied by one transactional operation on our server through the carer's own session.

What matters for privacy, because a health record is what is being edited:

Every active member can correct, the same as every active member can already edit a log — the group is the trust boundary here, as everywhere in the tracker.

What it inherits, stated rather than left to be assumed — because a correction touches the most sensitive rows in the app, and "nothing changes" is a claim that has to be made explicitly to be checkable:

Structural versus conventional, for this change specifically. Structural: the same-baby and same-group refusal, enforced inside the operation rather than by the screen not offering it; the whole correction succeeding or failing as one, so a half-applied regroup cannot exist; rows moving in place, which is what preserves both attribution and the deletion cutoff; the existing access rules, which are not re-derived. Conventional, and to be test-pinned: the preview showing what the result will be before it is confirmed; the exclusion of a manual regroup from the learned join window; and the absence of any clinical reading of the corrected totals — a regrouped feed produces a different number, and nothing may comment on whether that number is good.

The risk this adds, and the residual we accept. Risk: a correction is an edit to a health record, so a member can regroup entries into a sitting that never happened — a longer nursing duration, or a bottle counted once that was drunk twice — and the resulting total then appears beside the real ones with nothing distinguishing it. Who is harmed: the infant, if a carer later reads a wrong total as fact. Mitigation: component times, volumes, sources and attribution are preserved by the operation rather than recomputed, so a regroup rearranges observations instead of inventing one; the preview shows the result first; and the learned join window is not trained on it, so one bad correction cannot propagate into how every later feed is grouped. Kind: structural for the preservation and the transactionality, conventional for the preview and the training exclusion. Residual, accepted: a member who wants to record a feed that did not happen can already do so by typing one, and a correction is not a new capability so much as an honest route to a fix people currently achieve by deleting and retyping — which loses attribution and provenance, and is the worse record this change exists to prevent.

When this ships, this retrospective is revised against the code.

3.4 Planned: growth history, figure, CSV, and the assistant's growth card and figure (assessed 21 September 2026, not built)

Four things change, and each is assessed on its own because each fails differently. The design they implement is the growth-history plan in the repository's design documents, cited in §10.

(a) Reading a baby's growth history all the way back. Today every tracker surface reads a fourteen-day window of baby_logs. The history screen, a new growth screen and (d) below read one baby's date-level rows (growth, solids, immunisation) with no lower bound, capped and paged. No new data is collected and nobody new can read it: the read is the member's own session against the same SELECT policy (§4). What changes is that a device holds more of a child's record in memory while one of these screens is open; it is not persisted beyond the existing store window, and it passes through the client half of the deletion machinery (stripDeletedBabyRows) before it is shown, so a deletion this phone has asked for and the server has not yet applied hides the same rows the server will.

(b) A figure of the household's own numbers. Weight, length and head against the child's age, one panel each, drawn from (a). This is recall, and the line it must not cross is comparison: no percentile or reference curve, no target, no colour by value, no arrow that means "good". The referral sentence (§2) renders under it unconditionally, with a second sentence that the clinic holds the clinical record. Conventional, and pinned the way the medicine and temperature notices are: a vocabulary scan over every string the module can produce, and a presence test for the referral across a matrix of data shapes including the empty one.

(c) A CSV a person can open in Excel. Built on the member's device from rows already on it, written to the app's cache directory, handed to the operating system's share sheet, and deleted when the sheet closes. Columns: the day (group timezone), the child's age, each measure in kilograms/centimetres and in the stored grams/millimetres, the free-text note, the display name of the member who logged it. The row id is omitted (revised 3 November 2026). Structural: there is no server in this path — nothing about the export can be read, logged or retained by us, because it never reaches us. Code: free-text cells are escaped so a note beginning = cannot execute in a spreadsheet; the file is removed after sharing. Residual, accepted: from the moment the share sheet opens the file is in the hands of whatever the member chose, and the screen says so beside the button. Member names travel in the file — a decision taken when this was designed; they are attribution the household already sees on every row.

(d) The assistant. Two capabilities.

Revised 9 October 2026 — two follow-ups built on (a) and (c). Older stream entries: feeds, sleeps, nappies and the other timed types can now be read past the fourteen-day window on the history screen, one page per tap and never without one, by the same session against the same policy, with the same deletion strip — including the feed sittings those rows belong to. What changes is again only how much of a child's record a device holds while that screen is open; nothing is stored beyond it. Export of any one type: the history screen's CSV is (c)'s file for whichever single type is filtered — the rows listed, built on the device, deleted after the share sheet, free text escaped, the screen saying beside the button that a shared file is in the hands of whatever it was sent to. No server is in either path.

What it inherits. Access: unchanged (§4). Encryption tier: unchanged — plaintext at rest; the CSV and the chat pointer neither improve nor worsen it and neither may be described as doing so. Retention and deletion: unchanged for rows; the CSV is transient by construction and the chat figure is a pointer the sweep reaches through. Residency: no new destination. The assistant paths run under the existing baby-domain terms (§3.1); the figure tool sends the model nothing it did not already have from the roster. Consent: show_recent_summary gates sending logs to a model, and none of this sends one — the figure is the viewer's own device reading rows RLS already grants, on the explicit-request basis get_recent_baby_logs states. Child accounts: the tracker is not a child account's surface; unchanged.

Structural versus conventional, for this change. Structural: no server code path inserts a growth row for the assistant (the write is a proposal, and the proposal's resolver is the last gate, in stored units); the figure pointer carries no data; the CSV never reaches a server; the model never sees a baby id, and the client re-verifies the pointer's id against its own roster before drawing. Conventional, test-pinned: the absence of verdict vocabulary on the screen, in the bubble, in the tool description and in the model's reply; the referral's presence; the escaping of free-text cells; the deletion of the exported file.

The risk this adds, and the residual we accept. Risk: a growth chart is the surface on which every family app draws a percentile curve, and parents will ask for one — of the screen and of the assistant. Who is harmed: the infant, through a parent reassured or alarmed by a curve the app has no standing to draw. Mitigation: the positioning decision of 2 September 2026, carried in the tool description, the prompt, the vocabulary scans and the design plan. Residual, accepted: a parent can still mistype a weight and confirm it, on the sheet or on the card; the edit screen gains the ability to correct growth values so the remedy exists, and the app draws no conclusion from the number either way.

When this ships, this section is revised against the code.

3.5 Built, not yet released: the tracker's words sealed on the phones (30 September 2026)

The review trigger in §9 — "data at rest being encrypted" — has fired for the words. Under the wider assessment of sealing the remaining free text (the content encryption assessment), the free text a household writes in the tracker will be sealed on its phones under the part of the group's key that also protects event notes and places. This has been built and merged, but it has not reached any phone: it takes effect once our next database release ships and a member's app has updated. Once that happens:

What this does not change: an operator can still read the structured record (§8), the three paths to a model in §3.1 are unchanged, and the consent that gates the summary is unchanged.

4. Who can see it

5. Deletion

baby_history_deletions (added 5 September 2026) lets a group delete one baby's history, or the baby, from that baby's own settings behind a typed confirmation. The design is unusual enough to be worth recording as a privacy property rather than an implementation note:

Account erasure removes an account's data through the cascade and the storage sweep, evidenced in the GCS erasure ledger; delete-account fails closed if the ledger row cannot be written.

baby_feed_due_acceptances is a narrower durability record. It remains after an Undo so a delayed or restarted client cannot replay the same action ref and recreate the feed. Members have no direct table privileges; the two RPCs enforce active membership, and the row is removed with the baby or group. Erasing the accepting or undoing member sets that attribution to null while retaining the household idempotency fact.

6. Risks and mitigations

Risk Who is harmed Mitigation Structural or conventional Residual
The app states a clinical value and a parent acts on it at 3am the infant No clinical values exist. Deleted, and the deletions are pinned by mutation-tested guards Structural-ish — an absence, held by tests that fail loudly Accepted. A future change could add one; the tests are the guard
The app reaches a conclusion about a reading the infant The evaluation layer is deleted; one neutral notice style; unconditional referral; a verdict-vocabulary scan over every producible notice Tests, mutation-tested Accepted
An operator reads an infant's health record the infant RLS bounds everyone except the operator. Nothing more — Accepted, and stated in the policy. Encrypting data at rest is the planned fix
A parent believes turning the summary off stops all model access the infant Nothing today — Accepted, and the most important copy risk on this surface. §3.1
Free-text notes reach a model on an explicit ask the infant Fenced as untrusted; allowlisted elsewhere; but get_recent_baby_logs returns notes Conventional Accepted — it is what the parent asked for
A separated ex-partner, still a member, reads logs about a medical appointment an adult; the infant Nothing. Group membership is the boundary — Accepted. Leaving the group is the remedy; per-member visibility inside a group is deliberately not built
A log is attached to another group's baby that infant Composite FK Structural —
An infant's record follows them into adulthood the child, later The deletion request path; per-item deletion; account erasure Structural where used Accepted. §7
The dormant immunisation columns still hold data a child Unread, RLS-guarded, blocklisted from the assistant Structural for access; the presence is unmitigated Accepted until the columns are dropped
Baby data crosses a border on every model path the infant Nothing today; moving processing to Australia is the plan — Accepted, must be disclosed (s 26)

7. Best interests of the child (exposure draft s 10)

The infant here has no view and will not have one for a decade, which is exactly why this section is not a formality.

What the child gets. Continuity of care between the people looking after them. Two carers who both know when the last feed was is a real safety benefit, and it is the reason the feature exists. The design work that made a feed a sitting rather than a row, and a log a closed length rather than a running timer, was all in service of the record being true — a wrong number in a health record is the harm, and "the app was confidently wrong about a baby" is the failure mode the architecture is shaped against.

What it costs them. A permanent, timestamped record of their infancy — feeds, nappies, medicines, temperatures, and their parents' notes — held in a database they never consented to, readable by an operator, and reachable by a model on a parent's request. They will one day be an adult with opinions about that.

Where the child's and the parent's interests diverge, and who won.

What the child is told. Nothing; they are an infant. The obligation runs to the guardians, and the app's answer is that every notice tells them the app is not checking anything and points them at a professional.

8. Residual risk we accept

9. Review trigger

10. Sources

In repo, verified 2026-09-07: lib/babyLogTypes.ts, lib/babyLogCards.ts (showsRecentSummary), lib/babyCareGuidance.ts (DISCUSS_WITH_A_PROFESSIONAL), lib/babyLogProposals.ts (REFUSED_BABY_LOG_KINDS), lib/babyInsightCues.ts, lib/babyHistoryDeletion.ts, agent-server/src/tools/babyTools.ts (the bypass and its reasoning), agent-server/src/babySummary.ts (ALLOWED_DETAILS), agent-server/src/server.ts (the 403), __tests__/baby_tracker_rls.test.ts, lib/__tests__/babyCareGuidance.test.ts; database changes adding show_recent_summary (17 August 2026), baby_history_deletions (5 September 2026), and the avatars bucket (15 August 2026); docs/walkthroughs/2026-09-07-removing-live-timers.md; our internal engineering rules on health content in the baby tracker, on a feed being a sitting rather than a row, on a nappy being a moment, and on deleting a baby's history being a request row applied twice; docs/privacy_posture.md §§1, 4.1, 4.2, 8.

In repo, verified 2026-09-21 (§3.4): docs/baby_growth_history_and_export_plan.md (the design), lib/babyGrowth.ts, lib/babyCsv.ts, lib/babyCsvExport.ts, lib/babyLogHistory.ts, lib/babyLogProposals.ts (resolveGrowth), components/Baby/GrowthChart.tsx, components/Assistant/GrowthFigureBubble.tsx, agent-server/src/babyFields.ts (GROWTH_MEASURES, parseGrowthDetails), agent-server/src/tools/babyTools.ts (the growth branch of log_baby_event, show_baby_growth), agent-server/src/aggregate.ts (collectFigures), agent-server/checks/babyFigures.check.ts, agent-server/checks/babyLogTypes.check.ts (the unit-factor comparison).

Not re-read for this document: the Children's Code exposure draft itself; its section numbers here come from docs/privacy_posture.md §4.2.