# Shared-device switching (PIN) Design decisions for children sharing a family computer/tablet — recorded 2026-09-11. Implements: multi-session cookies, `pins` collection, profile picker, and the idle lock. ## Model - **The PIN is NOT a login.** It selects among sessions that already exist on the device. A PIN alone mints nothing on a fresh device; a stolen cookie alone selects nothing without the PIN. Threat model: sibling mischief on a family device (devtools-level bypass is explicitly accepted — data is family-scoped and actions are reversible). - Per-device state lives in cookies: - `pb_token_` — one httpOnly cookie per child with a session on this device (the "device is logged into both accounts" property). - `pb_active` — which child session is currently active. - `pb_token` — unchanged single session for parents (and legacy children). Parent login supersedes kid mode by clearing `pb_active`; a kid switch shadows (but does not delete) a parent session; resolving order is `pb_active` first, then `pb_token`. - Sessions are never the security boundary: the OTP gate still guards each child's first-ever join on a device, and switching re-mints an expired token server-side via the derived password (never exposed to the client). ## `pins` collection - Superuser-only rules (like `otp`); all reads/writes via server endpoints with session-role checks. Plaintext 3-digit PIN — deliberately recoverable so a parent can read it out (Q1: View, not reset). Child changes their own PIN in Preferences (requires current PIN). - Fields: `famId` (rel), `userId` (rel), `pin` (text). ## Switch flow `POST /api/switch-user { userId, pin }`: 1. Request must carry `pb_token_` (the profile must be on this device). No session required — the picker is reachable with no active user. 2. PIN verified server-side via superuser (children must NOT be able to read each other's pins, so collection rules can't be the check). 3. In-memory rate limit: 5 fails → 30s lockout per userId (3-digit space). 4. Refresh the device cookie's token; if expired, re-auth with the derived password (heals sessions on switch). 5. Set `pb_token_` + `pb_active`; client full-reloads to that child's dashboard URL (`/famSlug/handle`). Per-profile removal: `POST /api/device/remove { userId }` — deletes that cookie (+ `pb_active` if it was active). No session required (device-local cookie surgery only). ## Picker One component (`SharedPicker`) used everywhere: - TopNav switcher button (always visible when the device has child sessions). - Idle-lock return (overlay). - `/{famSlug}/switch` — standalone landing when the device has child sessions but no active session (e.g. after per-profile logout). The fam layout redirects unauthenticated device requests to it. Links: "Add a child with a code" → `/{famSlug}/join`, "Parent login" → `/login`. Every pick (including "resume" as the current kid) requires the PIN. ## Idle lock - Default 10 min; parent-adjustable in Family Settings: `Off / 2 / 10` (`settings.lockMins`, 0 = off; `shared-device.md` settled default 10). - Client-side only, child sessions only (parent sessions never lock). - **Only fires when shared-computer mode is ON for the device** (see below) — a personal device never locks, even with lockMins set. - `localStorage['fam_last_active']` updated on click/key/touch (throttled 10s) and force-written on `pagehide` — survives browser close, sleep and restarts. Next launch compares elapsed time; past the timeout → picker. Live timer (15s interval) does the same mid-session. - Resets: join wizard and picker switch write a fresh timestamp before navigating, so freshly-activated sessions don't instantly lock. ## Shared-computer toggle (TopNav) - **Storage: localStorage, NOT the DB.** Shared-ness is a property of *this browser* — a DB flag on `fams`/`settings` would force PIN mode on every device including a parent's phone. `localStorage['fam_shared_device']` is the source of truth; a plain `fam_shared_device=1` cookie mirrors it so server loads see it (`data.sharedDevice`). Neither is a security boundary — the PIN + device session cookies remain the gate. - **Location:** monitor icon in the TopNav (fam layout children slot, next to the profile switcher). Rendered for any role, even logged out — whoever holds the device can mark it shared. Active state: indigo fill. - **What it gates:** - Profile switcher (`kid-switch`) button — only shown when shared mode is ON and the device holds child sessions. - Idle lock — only fires for child sessions on shared devices. - (Server `/switch` redirect unchanged — the picker is harmless on - non-shared devices and the server can't do better without the cookie.) - **PIN reminders on enable** (`GET /api/pins/status`, set/unset only — values never leave the server except via the settings `revealPin` action): - Parent toggling sees the roster: who still needs a PIN (→ Settings → Members) vs who is ready (→ read the PINs out to them). - A child toggling sees only their own state: "remember your PIN" or - "ask a parent to set one up" (no PIN → picker shows "ask a parent"). - Client flag helpers: `lib/client/shared-device.ts` (`isSharedDevice` / `setSharedDevice`, writes the cookie mirror too). ## Join flow (child) After OTP redeem: 1. If the device already has other child sessions → PIN setup (required). 2. Else ask "Will this computer be shared with a sibling?" — yes → PIN setup; no → skip (PIN can be added later in Preferences; parents can set it in Family Settings). 3. Land on the child's dashboard. Parents keep the existing join (set own password, single session). ## Edge cases - Two tabs share the timestamp → the active tab keeps the other from locking. - Tab switches don't lock (kids alt-tab constantly). - Expired sessions: the picker still shows the profile; switching re-mints. - No PIN set on a profile → picker shows "ask a parent to set it up".