Frontend Runtime Flow
This document describes how the web app boots and where runtime responsibilities live.
Boot Sequence
Primary entrypoint:
packages/web/src/index.tsx
Boot order:
- initialize local storage through
initializeDatabaseWithErrorHandling() - initialize session tracking with
sessionInit() - render
<App /> - show a toast if local database initialization failed
This order matters because storage should be ready before listeners and repositories perform local operations.
App Provider Tree
packages/web/src/components/App/App.tsx renders:
- keyboard and movement event setup hooks
- optional providers
- required providers
- router provider
The route tree lazily loads feature views.
Router Flow
Files:
packages/web/src/routers/index.tsxpackages/web/src/routers/loaders.ts
Important behavior:
- the root route loads
RootView - the day route redirects to today's date when needed
loadAuthenticated()checks whether a session exists- route loaders use shared date parsing from
core
Root View Responsibilities
packages/web/src/views/Root.tsx:
- blocks mobile-OS devices with
MobileGate(isMobileOSuser-agent check; narrow desktop windows get the responsive layout instead) - wraps authenticated layout with
UserProvider - wires SSE listeners through
SSEProvider
This is the shell for the main desktop app experience.
Session Runtime
File:
packages/web/src/auth/session/SessionProvider.tsx
Responsibilities:
- initialize SuperTokens recipes
- track auth state in a
BehaviorSubject - mark users as having authenticated
- open or close the SSE stream on session changes
- expose a React context for auth status
Important detail:
Once a user has ever authenticated, the app records that fact in local auth-state storage so repository selection can prefer remote data later.
When a user re-authenticates with Google, auth-state utilities also clear any in-memory "Google revoked" flag so normal remote sync can resume.
Google Authorization Redirect
Google sign-in/up and Google Calendar connect/reconnect leave Compass through a full-page Google redirect and return through /auth/google/callback.
Before redirecting, the web app stores a short-lived authorization intent in sessionStorage keyed by OAuth state. The callback validates that state, finishes the saved intent, removes it, and returns the user to the original same-origin path or /day.
The old blocking overlay is not used for Google authorization.
User Bootstrap
File:
packages/web/src/auth/context/UserProvider.tsx
Responsibilities:
- fetch the user profile only for users who have authenticated before
- avoid blocking unauthenticated users
- show a session-expired toast on auth failures
- identify the user in PostHog when enabled
Client Version Polling
Files:
packages/web/src/components/Sidebar/SidebarActions/useVersionCheck.tspackages/web/src/components/Sidebar/SidebarActions/SidebarActions.tsx
Runtime behavior:
- version checks are disabled in development mode
- in non-dev mode, the client checks on mount, then every 5 minutes
- the client also checks when a tab returns to visible after being hidden for at least 30 seconds
- requests use an absolute URL built from
window.location.origin(/version.json?t=<timestamp>) with no-store/no-cache fetch options - checks are de-duplicated so concurrent visibility/interval triggers do not issue overlapping fetches
When the server version differs from BUILD_VERSION, isUpdateAvailable becomes true and the sidebar shows a refresh action that triggers window.location.reload().
Sidebar Footer Controls
Files:
packages/web/src/components/Sidebar/Sidebar.tsxpackages/web/src/components/Sidebar/MonthPicker/MonthPicker.tsxpackages/web/src/components/Sidebar/SidebarActions/SidebarActions.tsxpackages/web/src/components/Sidebar/ShortcutsOverlay/ShortcutsOverlay.tsx
Layout contract:
- the sidebar is fixed at 285px wide and fills the viewport height
- the scrollable planning content reserves its own scrollbar gutter so the footer stays fixed
- the footer control row is pinned to the bottom of the sidebar
- footer actions are grouped into shortcut access on the left and utility actions on the right
Control mapping:
- Open shortcuts opens an in-sidebar keyboard shortcuts overlay.
- Command palette toggle (
modifier + K) calls open/close palette actions from the settings Zustand store (packages/web/src/settings/settings.store.ts). - Refresh appears only when
useVersionCheck()reports an available update. - The account row shows temporary-account or signed-in account context.
- Background Google import state is not shown in the sidebar footer.
Icon state constraints:
- shortcut and command icons use filled weight when their related overlay/palette is open
- shortcut overlay state should not replace the sidebar conceptually; closing the overlay returns to the same sidebar
Dedication Dialog Runtime
Files:
packages/web/src/views/Week/components/Dedication/Dedication.tsxpackages/web/src/views/Week/WeekView.tsxpackages/web/src/views/Day/view/DayViewContent.tsx
Runtime behavior:
- the dialog is mounted in both day and week roots, so the same dedication UI is reachable in both views
ctrl+shift+0toggles the dialogescapecloses the dialog only when it is open- the component uses native
HTMLDialogElementAPIs (showModal,close) instead ofreact-modal
Transition/close contract:
- opening calls
showModal()first, then setsisVisibleinrequestAnimationFrameso CSS transitions can animate from hidden -> visible - closing sets
isVisibletofalseand waits foronTransitionEndbefore callingdialog.close() onCancelcallspreventDefault()and routes through the same close path so Escape/cancel actions do not skip exit animations
Pitfalls:
- do not call
dialog.close()directly in new close handlers unless you intentionally want to bypass the fade/scale exit animation - keep imports pointed at
.../Dedication/Dedication(no barrel file in this folder)
State Systems
The web app uses multiple state layers:
| Concern | Use | Key files |
|---|---|---|
| Event loading, fetching, read errors, and persisted entities | TanStack Query range caches | packages/web/src/events/queries/ |
| Event create/edit/delete/convert/reorder state | TanStack Query mutations | packages/web/src/events/mutations/ |
| Draft Event and calendar interaction state | Zustand draft store | packages/web/src/events/stores/draft.store.ts |
| View dates/sidebar, cmd palette, user metadata | Zustand stores | packages/web/src/events/stores/view.store.ts, packages/web/src/settings/settings.store.ts, packages/web/src/auth/state/user-metadata.store.ts |
| Offline persistence | IndexedDB offline data store | packages/web/src/common/storage/offline-data/indexeddb-offline-data.store.ts |
| Local vs remote persistence choice | Repository factory | packages/web/src/events/repositories/event.repository.util.ts |
These layers are intentional. Do not mirror persisted Event entities into the Zustand stores or call IndexedDB directly from components.
Zustand stores follow one pattern: a state-only store created with
create()(devtools(...)) plus module-level action functions (e.g.
draftActions.discard()) that work identically from React and non-React code.
Selectors are plain functions passed to the store hook
(useDraftStore(selectIsDrafting)); selectors must return primitives or
stable references (use useShallow if one ever builds a new object).
Read these together for event work:
packages/web/src/events/queries(reads, cache utilities, and view models)packages/web/src/events/mutations(persisted writes and pending state)packages/web/src/events/stores/draft.store.ts(transient drafts only)
Event Flow
For a high-level tour of the caching model (cache-key anatomy, reads, optimistic writes, and what refreshes the cache), see Event Caching. The summary below is the runtime sequence.
Typical event read flow:
- a view hook mounts a
useXEventsQueryhook (day/week) - TanStack Query fetches via the pure query function against the repository
for the reactive source (
event.repository.source.store.ts) - the normalized result remains in the source- and range-aware query entry; pure view models derive render data directly from it
- changing the view range re-keys the query (fetch on new ranges, instant
render from cache on revisits within
staleTime)
Typical event mutation flow:
- a hook or interaction calls the narrow
EventMutationsinterface - the mutation captures the active repository source and cancels Event reads
- immutable cache utilities apply the optimistic update to matching ranges
- failures only report the error (no rollback — a snapshot restore could
clobber a newer concurrent edit); the last settling mutation invalidates
eventQueryKeys.allso the refetch converges to canonical data - pending Event IDs derive from TanStack Query mutation state; they never block interaction — the sidebar account summary shows a syncing spinner
- SSE events invalidate the relevant query scope (day/week) to refetch later; auth transitions refresh the source store and drop stale cache entries
Creation uses optimistic events: the UI may show a temporary _id before the
repository returns the durable event. Do not store optimistic ids in other state
or treat them as stable.
Important consequence:
- persisted Event behavior is owned by TanStack Query; the draft Zustand store owns only draft and interaction state
- when debugging, inspect the query key, cache utility, mutation lifecycle, and repository source together
Styling Systems
The web app currently uses two styling systems in parallel:
- Tailwind utilities for component styling
- Tailwind v4 utilities and semantic theme tokens from
packages/web/src/index.cssfor newer or migrated surfaces
Use the existing c-* component utility convention and semantic colors from packages/web/src/index.css. Runtime theme values belong in --compass-* CSS variables so alternate themes can override values without rebuilding component styles.
Week Grid Drag Interaction
Dragging a saved event on the week/day calendar grid resolves the target day from a layout cache built at drag start, not from the event's own date arithmetic. See Week Drag Interaction for the coordinate model and why it matters once the week view can render fewer than 7 days.
Repository Selection
File:
packages/web/src/events/repositories/event.repository.util.ts
Repository choice:
- if Google access is revoked in-session, force local IndexedDB repository
- otherwise, never-authenticated users use local IndexedDB repositories
- authenticated or previously-authenticated users use remote repositories
This is deliberate and prevents events from "disappearing" after login when local data is empty.
Revoked state details:
- stored in memory only (not persisted)
- set when
GOOGLE_REVOKEDis detected from SSE or API error responses - cleared when Google auth succeeds again
Storage Initialization
Files:
packages/web/src/common/storage/offline-data/offline-data.store.registry.tspackages/web/src/common/storage/migrations/migrations.ts
Startup storage flow:
- create or reuse the offline data store singleton
- open IndexedDB and run internal schema migrations
- run data migrations
- run external import migrations
Database init failure is non-fatal; the app falls back to remote-only behavior when possible.
SSE Runtime
Files:
packages/web/src/sse/provider/SSEProvider.tsxpackages/web/src/sse/hooks/useSSEConnection.tspackages/web/src/sse/hooks/useEventSSE.tspackages/web/src/sse/hooks/useGcalSSE.ts
Responsibilities:
- open/close
EventSourcetoGET /api/events/streambased on auth state - refetch events when background event changes arrive (
EVENT_CHANGED) - react to Google import progress and Google revocation events
- apply
USER_METADATApushed on stream connect and when the backend refreshes metadata
Runtime nuances:
useGcalSSEusesUSER_METADATAas the source of truth for sync metadata and Google connection status.- auto-import is triggered only when
sync.importGCal === "RESTART"andgoogle.connectionStateis neitherNOT_CONNECTEDnorRECONNECT_REQUIRED. - On connect, backend may proactively send
GOOGLE_REVOKED; the client clears Google-origin events and falls back to local event storage until reconnect.
Google Connection UI Contract
Files:
packages/web/src/auth/google/hooks/useConnectGoogle/useConnectGoogle.tspackages/web/src/auth/google/hooks/useConnectGoogle/useConnectGoogle.util.tspackages/web/src/components/Sidebar/CalendarList/CalendarListHeader.tsxpackages/web/src/components/CommandPalette/hooks/useCalendarSyncCmdItems.ts
UI state comes from a single server-enriched metadata field (google.connectionState) plus two client-only states (checking, repairing). The sidebar's calendar-list heading (the account email, or "Temporary account" when anonymous) is the lightweight status indicator: it renders with a c-sync-text-wave shimmer whenever getGoogleSyncStatus(state)?.variant === "syncing" or an event mutation is still pending, and an sr-only role="status" live region announces "Syncing…" alongside it. CalendarListHeader does not render per-state tooltips or actions itself.
Detailed sync status and the reconnect/sync actions live in the command palette instead, via useCalendarSyncCmdItems/getGoogleConnectionConfig:
HEALTHY→ no command action (syncStatus.textis "Calendar up-to-date")ATTENTION→ Sync Google Calendar command action (onRepairGoogle);syncStatus.textis "Calendar is out of date"RECONNECT_REQUIRED→ Reconnect Google Calendar command action (onConnectGoogle);syncStatus.textis "Calendar needs reconnecting"NOT_CONNECTED→ Connect Google Calendar command action (onConnectGoogle); nosyncStatuschecking/repairing/IMPORTING→ no command action;syncStatus.textis "Syncing calendar…"
User-facing copy avoids the word "repair" — the ATTENTION state is framed as needing a sync, not a repair.
Important constraint:
connectionStatevalues are uppercase string literals shared with backend/core (NOT_CONNECTED,RECONNECT_REQUIRED,IMPORTING,HEALTHY,ATTENTION); lowercase variants will not match UI state guards.
Connect-later guardrail:
- In the password-session "connect Google" flow,
useConnectGooglecallssyncPendingLocalEvents(dispatch)beforeAuthApi.connectGoogle(...). - If local sync fails, connect is aborted and a toast is shown:
"We could not sync your local events. Your changes are still saved on this device." - This prevents IndexedDB-only Compass events from disappearing during the Google-triggered metadata/import refresh.
What To Read Before Editing
- Auth/session issue: read session provider, user provider, router loaders.
- Event refresh issue: read the SSE hooks (which invalidate query scopes), the
useXEventsQueryread hooks,event.query.options.ts, anduseEventMutations.ts(which invalidates after settlement). - Offline issue: read storage adapter and migration runner.
- Rendering issue in day/week: start at the route view, then its hooks.