Google Sync And Server-Sent Events (SSE)
Compass Google Calendar integration is bidirectional:
- Compass-originated event changes can propagate to Google and then notify web clients.
- Google-originated changes can flow back into Compass and then notify web clients.
Realtime updates use Server-Sent Events (one HTTP connection per tab, server pushes named events). The browser EventSource connects to GET /api/events/stream with the session cookie.
For local development, keep one boundary clear: browser API/SSE traffic can use localhost, but Google-to-Compass watch notifications are server-to-server POSTs from Google. Those notification POSTs need a public HTTPS backend URL if you want Google-side changes to arrive without a manual repair/import.
High-Level Architecture
Connection And First Events
Shared Event Names
Source:
packages/core/src/constants/sse.constants.ts
Wire format uses the event: field (uppercase identifiers). Backend and web both import these constants.
| Constant | Role |
|---|---|
EVENT_CHANGED | Calendar grid data should be refetched |
USER_METADATA | Replay / push SuperTokens + sync metadata |
IMPORT_GCAL_START | Full or repair import started |
IMPORT_GCAL_END | Import finished (see payload contract below) |
GOOGLE_REVOKED | Google refresh token invalid; client prunes state |
IMPORT_GCAL_END Payload Contract
Source:
packages/core/src/types/sse.types.tspackages/backend/src/user/services/user.service.tspackages/backend/src/sync/services/google-sync/google-sync.service.ts
IMPORT_GCAL_END carries an explicit operation so the client can distinguish repair completion from incremental completion.
type ImportGCalOperation = "INCREMENTAL" | "REPAIR";
type ImportGCalEndPayload =
| {
operation: ImportGCalOperation;
status: "COMPLETED";
eventsCount?: number;
calendarsCount?: number;
}
| {
operation: ImportGCalOperation;
status: "ERRORED" | "IGNORED";
message: string;
};
Operational constraints:
- repair path (
repairGoogleCalendarSync) emitsoperation: "REPAIR" - incremental path (
importLatestGoogleCalendarChanges) emitsoperation: "INCREMENTAL" - web listeners should keep a defensive
payload?handler for compatibility with older emitters/tests
Outbound Flow: User Changes An Event In Compass
High-level path:
- 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.
EventControllerpackages the change as aCompassEvent.CompassToGoogleEventPropagation.processEvents()loads the DB event, plans work, and applies persistence inside a retrying Mongo transaction, then runs Google side effects after commit (see Event Propagation Transactions).- After the Google effects, the backend calls
sseServerto publish anEVENT_CHANGEDnotification.
Primary files:
packages/web/src/events/mutations/useEventMutations.tspackages/web/src/events/repositoriespackages/backend/src/event/controllers/event.controller.tspackages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation.ts
Inbound Flow: Google Notifies Compass About Changes
High-level path:
- Google posts to the notification endpoint in sync routes.
publicWatchNotificationIngressvalidates and parses the Google headers.googleWatchService.handleGoogleWatchNotification()locates the watch and sync record.- The service builds a Google Calendar client for the user.
GCalNotificationHandlerfetches incremental changes using the stored sync token.GoogleToCompassEventPropagationapplies those changes to Compass data.- The backend publishes
EVENT_CHANGEDso clients refetch.
Primary files:
packages/backend/src/sync/sync.routes.config.tspackages/backend/src/sync/services/public-watch-notifications/public-watch-notification.ingress.tspackages/backend/src/sync/services/watch/google-watch.service.tspackages/backend/src/sync/services/google-sync/google-sync.service.tspackages/backend/src/sync/services/records/sync-records.repository.tspackages/backend/src/sync/services/notify/handler/gcal.notification.handler.tspackages/backend/src/sync/services/event-propagation/google-to-compass/google-to-compass.event-propagation.ts
Lifecycle and outbound repair paths live in:
packages/backend/src/sync/services/google-sync/google-sync.service.tspackages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google-backfill.tspackages/backend/src/sync/services/watch/google-watch-maintenance.service.ts
Notification Outcomes And Error Semantics
Same as before: recoverable INITIALIZED / IGNORED / PROCESSED paths, GOOGLE_REVOKED on invalid refresh token, etc. See inline comments in googleWatchService, googleCalendarSyncService, and SyncController.
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
USER_METADATAafter subscribe so reconnects get current state - exposes helpers (
handleBackgroundCalendarChange,handleImportGCalEnd, …) used by sync and error handling
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.tspackages/web/src/sse/provider/SSEProvider.tsx
The client:
- opens
EventSourcewhen a session exists (SessionProvider+SSEProvider) - refetches events when
EVENT_CHANGEDarrives (by invalidating the matching event query scopes) - tracks Google import status from
IMPORT_GCAL_*andUSER_METADATA - handles
GOOGLE_REVOKEDconsistently with REST error payloads
Refetches are driven by TanStack Query invalidation keyed to the SSE event names (EVENT_CHANGED, GOOGLE_REVOKED); USER_METADATA payloads land in the userMetadata Zustand store.
Revoked Token And Reconnect Lifecycle
- Backend detects missing/invalid Google refresh token (middleware, sync, or Google API error handling).
- Backend prunes Google-origin data and publishes
GOOGLE_REVOKEDover SSE. - Web app marks Google as revoked in session memory and temporarily switches to local repository behavior.
- User initiates re-consent via OAuth flow.
- Backend auth handler determines mode server-side; reconnect updates credentials and metadata.
User Metadata Shape Used By SSE And UI
UserMetadata includes Google connection state alongside sync state. It is pushed on stream connect (USER_METADATA) and available from GET /api/user/metadata.
Google Metadata Status Semantics
Source files:
packages/backend/src/user/services/user-metadata.service.tspackages/backend/src/sync/services/google-sync/google-sync.health.tspackages/core/src/types/user.types.tspackages/web/src/sse/hooks/useGcalSSE.ts
The sidebar Google connection state is derived from user metadata. The backend metadata service delegates HEALTHY vs ATTENTION diagnosis to Google sync health, which checks stored sync tokens and, when public HTTPS watch notifications are enabled, active Google watches.
Auto-import guardrail:
- client auto-starts import only when
sync.importGCal === "RESTART"andgoogle.connectionStateis notNOT_CONNECTEDorRECONNECT_REQUIRED
Calendar Import And Lifecycle
Compass imports every eligible Google calendar, not just primary. Five distinct
states apply to a Google-sourced CalendarRecord — a calendar can be true on
some and false on others independently (e.g. active + not currently watched,
if its last import attempt failed):
| State | Meaning | Source |
|---|---|---|
| Eligible | Present in Google's CalendarList with deleted !== true and hidden !== true. Determines whether Compass keeps a CalendarRecord at all. | getCalendarsToSync (sync/services/init/google-sync-init.ts) |
| Active | CalendarRecord.isActive. Reconciled from the eligible list on every CalendarList sync; a calendar that drops out of a later, complete list is archived (isActive: false), never deleted — its identity/preference row survives (A16). | calendarService.initializeGoogleCalendars (calendar/services/calendar.service.ts) |
| Visible | CalendarRecord.isVisible. A Compass user preference, seeded from Google's selected only on first insert and never overwritten by a later sync (A3). | mapGoogleCalendar (calendar/calendar.record.mapper.ts) |
| Writable | Derived from CalendarRecord.access (owner/writer writable; reader/freeBusyReader not). Not a stored field. | getCalendarCapabilities (packages/core/src/types/calendar.contracts.ts) |
| Watched | Compass currently holds a live Events webhook subscription. Only active, event-capable (owner/writer/reader) calendars whose most recent import succeeded are watched; freeBusyReader calendars are never watched (no Events resource to subscribe to). | initializeGoogleCalendarSync (sync/services/google-sync/google-sync.service.ts) |
Event import mirrors access role: owner/writer/reader calendars get their
events imported (with per-calendar bounded concurrency and a resumable,
per-calendar sync token); freeBusyReader calendars get only a CalendarRecord
— their live availability comes from a bounded freeBusy.query contract, never
synthetic events. One calendar's import failure is isolated and does not abort
the others (only a primary-calendar failure fails the whole sync); a failed
calendar retains whatever pages it already durably imported and can be retried
without duplicating work.
Import Flow
- Backend starts import.
- SSE publishes
IMPORT_GCAL_START. - Client reacts to metadata /
USER_METADATA/IMPORT_GCAL_END. - Backend completes import and publishes
IMPORT_GCAL_END. - Client stores import results and triggers a refetch when appropriate.
Manual Import Trigger Contract
POST /api/sync/import-gcal returns 204 immediately; progress is asynchronous via SSE events (not polling).
Debug
- Local debug dispatch of calendar-change notifications may use env
SSE_DEBUG_USER(seesync.debug.controller).
Rules Of Thumb For Changes
- New realtime behavior usually needs changes in
core(sse.constants/sse.types),backend(sse.server+ callers), andweb(hooks listening viaEventSource). - If you add a new SSE event, update shared constants and both emit/listen sides.
- If the UI is stale after edits, confirm an SSE event is published and the sync slice handles it on the client.