Repository navigation
spike: multi-account switching is session-cookie-only under Bearer - G10 #60
Description
Summary
The web account menu ends with Switch to ▸ — an account switcher. macOS has none, and cannot build one today: the entire account family is session-cookie-only.
The blocker (re-verified 2026-09-05, still true in the 2026-09-07 spec)
| Route | x-auth-type |
Result under Bearer |
|---|---|---|
GET /api/auth/accounts |
session |
401 |
POST /api/auth/switch |
none/session-driven |
unusable without a cookie session |
POST /api/auth/remove-account |
session |
unusable |
A Bearer-only client cannot enumerate, switch, or remove accounts. This is the same family as the other session-only routes in the backend-asks issue.
The spike question
Resolve the Bearer-vs-session constraint before designing anything:
- Can the client drive a real cookie session for just these routes (sign in via
POST /api/auth/login, hold theinterlinedlist_sessioncookie, use it only here)? What does that cost in complexity and in Keychain/cookie-store handling under App Sandbox? - Or should a Bearer variant be requested upstream? A per-account sync token is arguably the more natural native model anyway — several tokens in the Keychain, switch by swapping which one the client sends, no server-side "current account" concept at all.
Option 2 may make the whole server-side switch mechanism unnecessary for native. Establish that before writing code, because the two designs share almost nothing.
Deliverable: a written recommendation in docs/spikes/.
What the implementation would involve, once unblocked
Domain / Kit
- Per-account credential storage —
KeychainCredentialStorekeyed by account, not a single slot. - An account-switch operation with a clean cache reset.
App
- An account switcher in the account menu / sidebar Profile group.
-
⚠️ Cache reset on switch is the risk. The app carries an on-disk SWR cache for Lists, Documents, Scheduled, Organizations, and Messages, plus aLaunchPrefetchCoordinator. Every one of those stores must be scoped per account or cleared on switch — otherwise account A's cached lists paint under account B's session, which is a data-leak-shaped bug, not a cosmetic one. Design the cache scoping before the switcher UI. - The Document Sync Agent reads a token from the shared Keychain group — decide what an account switch means for a running agent that is mirroring one account's documents to a local folder. This is not obvious and needs an explicit answer.
Tests
- happy — switch between two accounts; each sees only its own content
- invalid — switch to an account whose credential has been revoked
- upstream-failure — switch fails mid-way → the app stays signed in as the original account, not signed out
- boundary — one account (switcher hidden); removing the currently-active account
Acceptance criteria
- The spike's recommendation is written down.
- If built: no cached artifact from one account is ever visible under another.
- Full E2E gate green.
Notes
work-consolidation.md tracks this as G10 / spike S4. Blocked on the session-only route family — see the backend-asks issue. Size M after the spike.
Activity
Adron commentedon Sep 16, 2026
Still blocked, and the re-measure confirmed rather than relieved it
Re-checked against the 2026-09-14 re-measure recorded on #66.
That re-measure was good news almost everywhere — the session-only set shrank from 45 operations to 32, and 28 of those are
/api/admin/*,/api/architecture-aggregates/*and/api/stripe/*that a native client should never call. The Dashboard stopped being blocked because of it (see #61).This issue is in the residue. Three of the four genuinely limiting routes are exactly the ones multi-account switching needs:
Controls were probed the other way during that sweep:
/api/auth/accountsreturns 401 under a valid Bearer sync-token. So this is not an annotation error of the kind corrected on/api/user/identities(PR #93) or/api/user/organizations(G25) — it was tested under the transport in question and it really does reject Bearer.Why the obvious workaround is wrong
The kit does have a cookie-session transport with a lazy login (
AuthTransport.swift), so these routes are technically reachable. That is not a solution:What we need from you
This is a backend ask, and it belongs with #58's list:
If the answer is no, the honest macOS story is one account per app instance, and that should be written up in
docs/decisions/so a future sweep finds the decision rather than re-opening the question — the same treatment #49 got.Nothing here is implementable until that answer arrives.