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 |
SOMEDAY_EVENT_CHANGED | Someday sidebar 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 dispatches an event action.
- A saga performs optimistic updates.
- The selected repository writes locally or remotely.
- Remote event writes hit backend event routes.
EventControllerpackages the change as aCompassEvent.CompassToGoogleEventPropagation.processEvents()loads the DB event, plans work, applies persistence, and runs Google side effects.- After commit, the backend calls
sseServerto publish notifications based on whether the change affected normal or someday events (EVENT_CHANGEDvsSOMEDAY_EVENT_CHANGED).
Primary files:
packages/web/src/ducks/events/sagas/event.sagas.tspackages/web/src/common/repositories/eventpackages/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_CHANGED(or someday equivalent) so 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_CHANGED/SOMEDAY_EVENT_CHANGEDarrive (viaSync_AsyncStateContextReasonaligned with those names) - tracks Google import status from
IMPORT_GCAL_*andUSER_METADATA - handles
GOOGLE_REVOKEDconsistently with REST error payloads
Redux reasons for refetch (Sync_AsyncStateContextReason) reuse the same string values as SSE event names where they correspond (EVENT_CHANGED, SOMEDAY_EVENT_CHANGED, GOOGLE_REVOKED), plus app-local reasons such as IMPORT_COMPLETE.
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
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.