Multi-child shared device: persona binding owns device-side child identity; current-child selection stays app-local
Context
E02 PRD v2.4 (2026-06-08, Confluence comment 35946533) established the pairing
principle: "each Container is bound to a specific child; when placed on the
Device, YiBi recognizes that child — not is_current". E01 PRD v4.1 states the
same as design principle #9: "the persona (Container) determines child
identity; the device is a family-shared carrier".
The previous architecture conflicted with this on three points:
devices.child_profile_idmade the device a 1:1 child-bound object — a multi-child family could not share one device with per-child personalization.- No Container→Child relationship existed anywhere.
container_bindingsonly maps PhysicalContainer→VirtualContainer (content), and the only child-related container table (container_first_touch, from change 0019) is greeting-audio bookkeeping, not identity. - The E02 spec left the read boundary of
is_current(the app's currently-selected child) underspecified ("前端可切換 active child" with no exclusion of device-side reads), and the old PRD §5.2.4 wording described it as an "E11 query basis", implying the device consults it — which contradicts the v2.4 pairing principle.
Constraints: devices are offline-first (ADR-0010) — any device-side mapping
must be available without cloud contact; deletion in E02 is Soft Delete
(deleted_at), so FK ON DELETE CASCADE alone cannot keep bindings honest.
Decision
Conclusion: Container→Child identity lives in PostgreSQL
persona_bindings (one row per container, UNIQUE on container) and is
projected to devices through the existing GET /config channel; the app's
current-child selection (is_current) remains app-local state and never
reaches the backend or the device.
Five sub-decisions:
- Storage shape — independent join table
persona_bindings(physical_container_idUNIQUE,child_profile_id,family_id,bound_at,updated_by), orthogonal tocontainer_bindings(content vs identity). Defined by change0023-e01-multi-child-persona-binding(PR #476, in review as of 2026-06-10; backend implementation in progress in a separate worktree, not yet merged). - Device sync — no new channel.
/configresponse (owned by change 0022) gains a top-levelpersona_bindings[]array ({physical_id → child_profile_id}); every binding write bumpsfamilies.config_version, so the existing ETag / wake-decision logic invalidates the device's NVS cache naturally. Offline NFC scan resolves the child from the local cache. devices.child_profile_idis dropped (BREAKING, with data migration that seedspersona_bindingsfrom existing device bindings). The device no longer has a child; it has a family.is_currentstays app-local — stored in SharedPreferences (survives app restart, resets on reinstall/new phone, falling back to the first child). No backend column, no PATCH endpoint. Switching the current child fires a one-shot event to E12 to reset AI context; nothing is persisted.- Soft Delete cascade — when a child is Soft Deleted, the service layer
actively DELETEs that child's
persona_bindingsrows in the same transaction (option (a)). The container reverts to "unassigned" and will be re-bound by order-dependent auto-assign on its next first scan.
Semantic split that must hold across all specs:
| Situation | Child context source |
|---|---|
| App browsing (home, library, profile) | app-local is_current |
| Container placed on device | persona_bindings.child_profile_id |
| Device idle (no container) | none — the device has no "current child" |
| E12 AI context switch | event fired on app-side switch, not persisted |
Alternatives Considered
physical_containers.child_profile_idnullable FK — simplest shape, but mixes identity into a manufacturing/inventory table, leaves no room for audit fields (bound_at,updated_by), and "unassigned" becomes a NULL instead of an absent row. Superseded by thepersona_bindingstable in change 0023.- Persist
is_currentto backend (child_profiles.is_current+ PATCH) — survives phone changes, but introduces multi-device write conflicts (phone A switches to 小晴 while phone B still shows 小宇) for a value no backend consumer actually reads after this ADR. Rejected per PRD §5.2.4 ("managed in App only"). - Store the binding in firmware NVS only (app writes it during pairing, cloud never knows) — satisfies offline use but makes family-scope validation, Soft Delete cascade, and E12/E08 reads impossible; binding would be lost on device reset. Rejected.
- Keep
devices.child_profile_idas a weak "primary child" reference (E02 discussion v1 position) — two sources of truth for the same question; change 0023 chose the clean BREAKING drop with data migration instead, and this ADR adopts that.
Consequences
好:
- One device serves any number of children; per-child personalization is driven by physical interaction (which persona was placed), matching the toddler mental model "this is my figure".
- Reuses the ADR-0010 offline-first transport unchanged — no new endpoint, no push channel, no broker.
is_currentstaying local means zero sync conflicts and no API surface for a purely presentational concern.
壞 / 代償:
- Binding changes are eventually-consistent on the device ("下次使用時生效", up to the next wake) — a parent who reassigns a persona and immediately hands it to the other child may get the previous child's content once.
- The same eventual consistency applies to Soft Delete: an offline device retains the deleted child's mapping in its local cache with no upper bound until its next wake (ADR-0010 offline-first). PRD §9.4's privacy guarantee is immediate at the backend boundary but unbounded-eventual on devices. Accepted risk; recorded in the E02 delta spec of change 0024.
- Soft Delete cascade deletes binding rows outright, so restoring a soft-deleted child (manual CS flow) does not restore persona assignments; parents must re-assign. Accepted: restores are expected to be rare.
- The E12 context-switch event is one-shot and unpersisted: if it is lost, the E12-side reset is delayed. Mitigation (change 0024 spec): App-issued AI requests always carry the current child_id explicitly, so a lost event delays the proactive reset but cannot misattribute AI context across children.
- Reinstalling the app resets the current-child selection to the first child.
中性:
container_first_touch(greeting audio, change 0019) andpersona_bindingsboth key on (child, container); their interaction order on a first scan is specified in change 0023/0022 integration, not here.- The E02 spec's "管理多個孩子" requirement must be rewritten to make the
is_currentread boundary explicit (the spec is currently underspecified; the "E11 query basis" wording lives in the old PRD §5.2.4, not the spec) — captured in follow-up change0024-e02-current-child-and-soft-delete-cascade.