Skip to main content

Multi-account sync invariants

These rules keep Compass's per-account Google sync state honest after email/password signup plus "Add account", reconnect, and missed SSE.

Zustand remains the store for user metadata. The failure mode was refresh discipline, not the store; migrating metadata to react-query would touch dozens of consumers (auth, sidebar, settings, SSE handlers, and the __COMPASS_E2E_STORE__ Playwright bridge) to fix a problem four files own. Calendars live in react-query and connections in zustand; refetching both unconditionally on the same signals keeps them from diverging.

1. Push on every transition​

refreshConnectionState is the only writer of derived connection state. On change it persists the new state and appends a kind: "connection" invalidation. The backend SSE bridge fans those invalidations out as calendarsChanged.

Any new job kind or route that changes state-derivation evidence (bootstrap flags, cursors, credentials, calendar-list discovery, durable read failures) must end by calling refreshConnectionState with the invalidations repository. Do not grow a second push channel. Delete the dormant importProgress invalidation kind and caller-less sseServer.publishUserMetadata in a follow-up cleanup rather than adding another path.

At dispatch time the completing job is still claimed, so derivation often lands on catchingUp first. That is still a change from importing, so the invalidation fires, the client refetches, and the read-path refresh after the job settles lands healthy.

2. Pull reconciliation, unconditionally cheap​

Force-refresh metadata on every SSE signal, on stream open/reopen, and on tab focus — regardless of connection state — plus a 20s poll while any single connection is connecting / importing / catchingUp.

The sync GET /connections read path re-derives state, so every metadata pull is also a server-side reconciliation. The UI self-heals even with SSE fully dead. Concurrent force refreshes chain onto one trailing fetch; an epoch counter drops stale writes.

Do not gate metadata reconciliation on HEALTHY/ATTENTION. That gate is only for the provider Refresh enqueue.

3. Per-account attribution​

User-visible status hangs off one connection, never the precedence-collapsed aggregate. A stuck account must not pin an unattributed banner (or disable reconciliation) for all accounts.

  • The local Compass calendar nests under the Google account whose email matches the signed-in user. When no connected account shares that email, it stays its own sidebar section (the signed-in user's email) once any Google account exists. It stays visible and toggleable. LCV1/LCV2 still exclude it as a create target.
  • A connected-but-still-importing account keeps its section header with zero calendars so "Adding your calendar…" attributes to that account.
  • Day view columns are active+visible calendars, matching Week.

Follow-up candidate: SidebarStatusBar names the account when more than one connection exists.