YiBi Dev Docs
架構決策 (ADR)

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:

  1. devices.child_profile_id made the device a 1:1 child-bound object — a multi-child family could not share one device with per-child personalization.
  2. No Container→Child relationship existed anywhere. container_bindings only maps PhysicalContainer→VirtualContainer (content), and the only child-related container table (container_first_touch, from change 0019) is greeting-audio bookkeeping, not identity.
  3. 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:

  1. Storage shape — independent join table persona_bindings (physical_container_id UNIQUE, child_profile_id, family_id, bound_at, updated_by), orthogonal to container_bindings (content vs identity). Defined by change 0023-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).
  2. Device sync — no new channel. /config response (owned by change 0022) gains a top-level persona_bindings[] array ({physical_id → child_profile_id}); every binding write bumps families.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.
  3. devices.child_profile_id is dropped (BREAKING, with data migration that seeds persona_bindings from existing device bindings). The device no longer has a child; it has a family.
  4. is_current stays 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.
  5. Soft Delete cascade — when a child is Soft Deleted, the service layer actively DELETEs that child's persona_bindings rows 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:

SituationChild context source
App browsing (home, library, profile)app-local is_current
Container placed on devicepersona_bindings.child_profile_id
Device idle (no container)none — the device has no "current child"
E12 AI context switchevent fired on app-side switch, not persisted

Alternatives Considered

  • physical_containers.child_profile_id nullable 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 the persona_bindings table in change 0023.
  • Persist is_current to 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_id as 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_current staying 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) and persona_bindings both 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_current read 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 change 0024-e02-current-child-and-soft-delete-cascade.

On this page