← All privacy impact assessments

PIA: the baby tracker

Status: Retrospective — written against the code as it stands on 2026-09-07, after the live-timer removal. The feature shipped long before this assessment; that is what "retrospective" means and there is no point pretending otherwise. Its value is the baseline it sets for the next change to this surface. Written: 2026-09-07 · Issues: #221 (register), #330 (parked health-service question) Nothing here is legal advice.


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_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 and parked by owner decision until the app has been tested with families we know (#330). 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, PR #286). 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_logslog_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 yesnotes, 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_care_guidancewhat 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. DISCUSS_WITH_A_PROFESSIONAL in lib/babyCareGuidance.ts, 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. lib/__tests__/babyCareGuidance.test.ts pins its presence across a matrix of readings deliberately including the unremarkable ones, and scans every notice the module 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 agent blocklists that refuse them to the assistant (lib/babyParameterTargets.ts, agent-server/checks/toolContracts.check.ts 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 tracked in ROADMAP.md.

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

Nothing here is encrypted at rest. lib/crypto/ exists and is unit-tested but is wired to nothing; the only ciphertext column in the schema is review_moderation_log. 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 #223 exists to change — baby notes are in the Tier 1 list.

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

group_babies.show_recent_summary (default false, 20260817130956) is ONE consent, and every ambient surface that sends baby logs to a model inherits it. It is read through showsRecentSummary() in lib/babyLogCards.ts, which fails closed: return baby.show_recent_summary === true.

Path What travels Gated by show_recent_summary?
Tracker summary cardagent-server /api/baby/summary An allowlisted digest built by digestFor (agent-server/src/babySummary.ts): only the detail keys in ALLOWED_DETAILS 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: getInsightModel()google/gemini-3.5-flash-lite. Yes, and re-checked server-side. agent-server/src/server.ts fetches the row and returns 403 if it is false, rather than trusting the client.
Home dashboard insightgenerate-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 readsget_recent_baby_logs and query_baby_stats in agent-server/src/tools/babyTools.ts get_recent_baby_logs returns up to 50 rows: id, log_type, started_at, ended_at, nappy_status, fluid_volume_ml, details, notesincluding the free-text notes, fenced as untrusted. query_baby_stats returns daily aggregates from get_baby_stats_time_series. No. These deliberately bypass the gate, and the code says so 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 comment in babyTools.ts 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. The consent ledger (#219, planned) 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. WS20 (planned) moves group data, including every baby surface, to Vertex in australia-southeast1. Until it lands, 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

Owner decision, 2026-09-05. 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 REFUSED_BABY_LOG_KINDS (lib/babyLogProposals.ts): 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).

4. Who can see it

5. Deletion

baby_history_deletions (20260905185915) 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.

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 agent 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. #223 is the 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 destructive migration
Baby data crosses a border on every model path the infant Nothing today; WS20 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 accepted by the owner

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; migrations 20260817130956 (show_recent_summary), 20260905185915 (baby_history_deletions), 20260815173615 (the avatars bucket); docs/walkthroughs/2026-09-07-removing-live-timers.md; AGENTS.mdHealth Content — Baby Tracker, A Feed Is A SITTING, Not A Row, A Nappy Is A Moment. A Feed Lasts., Deleting A Baby's History Is A REQUEST ROW, Applied Twice; docs/privacy_posture.md §§1, 4.1, 4.2, 8.

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.