Google Sync And Server-Sent Events (SSE)
Google Calendar sync is owned entirely by the standalone Sync service
(packages/sync) — the backend has no Google API calls or sync logic of its
own. The backend's role is: proxy sync-related reads/writes to Sync, poll
Sync's change feed, and translate what it learns into browser SSE.
Realtime updates use Server-Sent Events (one HTTP connection per tab).
The browser EventSource connects to GET /api/events/stream with the
session cookie.
High-Level Architecture
Connection And First Events
Wire Format And Message Types
Source:
packages/core/src/constants/sse.constants.ts— the SSE transportpackages/core/src/types/server-message.contracts.ts— the message union
The server publishes one SSE event name, message (SSE_MESSAGE_EVENT).
Its data is a JSON-serialized ServerMessage union member; clients parse
once and switch on type. There is no longer a distinct SSE event name per
signal (EVENT_CHANGED, IMPORT_GCAL_START, etc. are retired).
type | Role |
|---|---|
eventsChanged | Calendar grid data should be refetched |
calendarsChanged | Calendar list should be refetched |
syncStatusChanged | Google connection health changed (syncing/healthy/attention) |
importCompleted | A full/incremental/repair import finished |
userMetadataChanged | Replay / push SuperTokens + sync metadata |
SSEServer (packages/backend/src/servers/sse/sse.server.ts) exposes one
named publish* convenience method per message type, plus a generic
publish(). A completeness test
(packages/backend/src/servers/sse/sse.server.test.ts) enforces that every
publish* method emits a schema-valid frame — keep it exhaustive when the
ServerMessage union grows.
Outbound Flow: User Changes An Event In Compass
- UI calls a mutation from
useEventMutations. - The mutation's
onMutateapplies an optimistic update to the TanStack Query cache. - The mutation's
mutationFnwrites through the selected repository. - Remote event writes hit backend event routes, which submit a command to the Sync service (see Backend).
- The backend does not publish
eventsChangedfor its own write — the client already applied the change optimistically. Confirmation and cross-tab/cross-device fan-out come from the change-feed poll below.
Primary files:
packages/web/src/events/mutations/useEventMutations.tspackages/web/src/events/repositoriespackages/backend/src/event/controllers/event.controller.tspackages/backend/src/common/services/sync-service/(Sync client)
Inbound Flow: Sync Notifies The Backend About Changes
Google's own webhook ingress, OAuth flow, and import/repair logic all live
inside packages/sync and never touch the backend directly. The backend
learns about changes by polling Sync's own change feed:
- While a user has at least one open SSE connection,
SyncChangeFeedBridge(packages/backend/src/servers/sse/sync-change-feed.bridge.ts) pollsGET /internal/changeson the Sync service every ~2s. - Each page of invalidations is translated to zero or more
ServerMessages bysyncInvalidationToServerMessages(packages/backend/src/servers/sse/sync-invalidation.to-server-message.ts) — e.g. a Synceventinvalidation becomes aneventsChangedmessage, animportProgressinvalidation becomessyncStatusChanged/importCompleted. - The backend publishes each translated message over SSE.
- Separately,
pruneGoogleDataAndNotifyRevoked(packages/backend/src/common/services/gcal/google-revoked.util.ts) publishessyncStatusChangedwithcode: "GOOGLE_REVOKED"directly when the backend itself detects a revoked/missing Google grant.
Primary files:
packages/backend/src/servers/sse/sync-change-feed.bridge.tspackages/backend/src/servers/sse/sync-invalidation.to-server-message.tspackages/backend/src/common/services/gcal/google-revoked.util.tspackages/core/src/types/sync/change-feed.contracts.ts(the Sync-side invalidation shapes)
SSE Server Responsibilities
Source:
packages/backend/src/servers/sse/sse.server.tspackages/backend/src/events/controllers/events.controller.ts
The SSE layer:
- accepts authenticated
GET /api/events/streamrequests (SuperTokens session) - registers each open
Responseper user for fan-out - sends periodic comment heartbeats (
: keepalive) so buffering proxies do not delay events - on connect, replays
userMetadataChangedafter subscribe so reconnects get current state
Web Client Responsibilities
Files:
packages/web/src/sse/client/sse.client.tspackages/web/src/sse/hooks/useSSEConnection.tspackages/web/src/sse/hooks/useEventSSE.tspackages/web/src/sse/hooks/useGcalSSE.ts(+useGcalSSE.factory.ts)packages/web/src/sse/hooks/useSyncFocusRefresh.tspackages/web/src/common/hooks/useVisibleAfterHidden.tspackages/web/src/sse/provider/SSEProvider.tsxpackages/web/src/auth/google/hooks/useConnectGoogle/useConnectGoogle.tspackages/web/src/auth/google/state/google.sync.refresh.ts
The client:
- opens
EventSourcewhen a session exists (SessionProvider+SSEProvider) - refetches events when
eventsChangedarrives (by invalidating the matching event query scopes) - tracks Google sync/import status from
syncStatusChanged/importCompletedanduserMetadataChanged - handles the
GOOGLE_REVOKEDsyncStatusChangedcode consistently with REST error payloads - auto-refreshes Google Calendar sync on app focus (below)
Refetches are driven by TanStack Query invalidation keyed to the message
type; userMetadataChanged payloads land in the userMetadata Zustand
store.
Focus Refresh
SSEProvider mounts useSyncFocusRefresh, which calls the same
useConnectGoogle().refresh path as the sidebar Refresh calendar button:
- once when a refreshable connection becomes available on mount
- again whenever the tab returns to visible after being hidden for at least
30 seconds (
useVisibleAfterHidden; same threshold as version checks)
Constraints:
- runs only for
HEALTHYorATTENTIONconnections (no-op while disconnected, reconnect-required, or still on the initial import) - passes
silent: trueso a transient background failure does not toast the way a manual click's failure does - shares the browser-wide refresh coordinator with manual clicks, so focus and CTA refreshes coalesce instead of racing
Without this, a user can stare at a stale “Updated …” label until they remember to click Refresh calendar.
Revoked Token And Reconnect Lifecycle
- Backend detects missing/invalid Google refresh token (middleware or Google API error handling) or Sync reports it via the change feed.
- Backend prunes Google-origin data and publishes a
syncStatusChangedmessage withcode: "GOOGLE_REVOKED". - Web app marks Google as revoked in session memory.
- User initiates re-consent via the OAuth flow (proxied to Sync).
- Sync completes the OAuth exchange; the backend's next metadata fetch/change-feed poll picks up the reconnected state.
Rules Of Thumb For Changes
- New realtime behavior usually needs changes in
core(server-message.contracts.ts),backend(sse.server+ whichever translator/caller publishes it), andweb(hooks listening viaEventSource). - If you add a new
ServerMessagemember, add a matchingpublish*method onSSEServerand a case insse.server.test.ts's completeness table. - If the UI is stale after edits, confirm a message is actually published
(either via
sync-change-feed.bridge.ts's translation or a directsseServer.publish*call) and that the web hook handles thattype.