Zum Inhalt springen
Docs

Befehlspalette

Zu einer Seite oder Ansicht springen oder ein Todo per Name finden.

Änderungen

Family sync with anonymous device keys

The repository's architectural decision, rationale and consequences.

Markdown

Status: accepted · Date: 2026-11-06 · Related: ADR-0015, ADR-0017, ADR-0020

Enforcement: packages/api/src/features/family/family.procedures.test.ts (server stores ciphertext only, invite expires after 24 h, no tenant reads a family), packages/preparedness/src/outbound.family.test.ts (the snapshot excludes health data and the vault), apps/mobile/src/features/family/family.sync.test.ts (only ciphertext leaves the phone), family.flow.test.ts, family.shared.test.ts and family.merge.test.ts (members' edits, T38).

ADR-0020 expected family sync to be the first place the app asks for an email. T24 allows "Better Auth or anonymous device keys". We chose device keys; this amends ADR-0020's consequence.

Decision

  1. No account for family sync. POST /api/family makes a family whose only member is the calling phone (owner). Each phone holds a random 256-bit token; the server stores only its SHA-256, like the municipality opt-in. The token travels as Authorization: Bearer, never in a URL.
  2. Invites are QR codes: single use, valid 24 hours, granting edit (push and pull) or view (pull only; the app shows the plan). Only the owner invites and removes; anyone may leave; the owner leaving deletes the family and its blobs. At most 6 phones.
  3. End-to-end encryption. The owner's phone makes a random AES-256 family key. It goes from phone to phone inside the invite QR, never to the server. A change is the home's snapshot (buildFamilySnapshot in @varu/preparedness/outbound: people, pets, goal, member names, the split shopping list) sealed with AES-256-GCM, the family id as additional data.
  4. Health and vault never enter a snapshot. The builder copies an allowlist and drops the health area and the rules a health flag brings (medication, powered-device backup, pet medication). Vault documents are not an input at all.
  5. Storage. Sealed bytes go to the family-sync R2 bucket (FAMILY_SYNC_BUCKET, EU jurisdiction, same endpoint and credentials as STORAGE_*); Postgres holds only the object key, size, sequence and author. The newest 20 changes per family are kept. All four tables (family_group, family_member, family_invite, family_change) are system-only (asSystem(db, "family.sync", …)); app_tenant has no grant.

Why not Better Auth

An account would add an email and a User row per family member to a server that otherwise knows nothing about a home, and the key would still need an out-of-band channel. The QR already is that channel.

Consequences

  • A lost phone cannot be recovered into the family; the owner removes it and invites again. A leaked QR within its 24 hours lets one phone join; the owner sees the member count and can remove it.
  • The family key never rotates: a removed phone that kept old snapshots can still read them, and could read new ones if it got the blobs (it cannot: its token is gone). Key rotation on removal is future work.
  • Only the owner pushes snapshots (whole, sent only when they changed). Since T38 a member with edit access pushes edits instead (buildFamilyEdits: tick/untick and new buyer by row id, only for rows of the received snapshot, so never a health row). They are queued on the member's phone (SQLCipher settings) while offline and sent sealed like any change. The owner's phone merges them in sequence order (mergeFamilyEdits): a member's tick also records the row's quantity as stock, so readiness rises (F6); an untick takes back only that stock. Health rows are never changed from outside. A snapshot carries through, the last change it merged, so a member keeps showing its own edits until a snapshot includes them.
  • Production needs a second EU-jurisdiction bucket (see docs/STATUS.md).
Auf GitHub bearbeitenDie Dokumentation ist auf Englisch.