Skip to content

spike: multi-account switching is session-cookie-only under Bearer - G10 #60

Open

Description

@Adron

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:

  1. Can the client drive a real cookie session for just these routes (sign in via POST /api/auth/login, hold the interlinedlist_session cookie, use it only here)? What does that cost in complexity and in Keychain/cookie-store handling under App Sandbox?
  2. 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 — KeychainCredentialStore keyed 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 a LaunchPrefetchCoordinator. 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

  1. added
    parityWeb-parity gap with the InterlinedList web app
    blockedCannot proceed — backend-gated or spike-first
    spikeInvestigate first; scope unknown until then
    on Sep 7, 2026
  2. Adron commented on Sep 16, 2026

    @Adron
    MemberAuthor

    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:

    GET  /api/auth/accounts        x-auth-type: session
    POST /api/auth/switch          x-auth-type: session
    POST /api/auth/remove-account  x-auth-type: session
    

    Controls were probed the other way during that sweep: /api/auth/accounts returns 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:

    • Account switching under a cookie session would give the app two concurrent notions of who is signed in — the bearer token in the Keychain and the cookie jar — which can disagree after a switch.
    • Every other authenticated call in the app uses Bearer. A switch that only moves the cookie would leave the rest of the app on the previous account, which is a far worse failure than not offering the feature.

    What we need from you

    This is a backend ask, and it belongs with #58's list:

    Can GET /api/auth/accounts, POST /api/auth/switch and POST /api/auth/remove-account accept a sync-token Bearer? A native client has no cookie-session identity to switch, and switching the cookie alone would desynchronise it from the bearer token every other call uses.

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    blockedCannot proceed — backend-gated or spike-firstparityWeb-parity gap with the InterlinedList web appspikeInvestigate first; scope unknown until then

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions