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() - start redux-saga with
sagaMiddleware.run(sagas) - initialize session tracking with
sessionInit() - render
<App /> - show a toast if local database initialization failed
This order matters because storage should be ready before sagas 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 with
MobileGate - 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/common/hooks/useVersionCheck.tspackages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.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().
Calendar Sidebar Footer Controls
Files:
packages/web/src/views/Calendar/components/Sidebar/Sidebar.tsxpackages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsxpackages/web/src/views/Calendar/components/Sidebar/styled.tspackages/web/src/auth/hooks/google/useConnectGoogle/useConnectGoogle.ts
Layout contract:
- the footer control row is pinned to the bottom of the sidebar (
ICON_ROW_HEIGHT = 40) SidebarTabContainerreserves space withheight: calc(100% - 40px)so tab content does not overlap the footer row- the row uses
justify-content: space-betweenand two explicit groups:LeftIconGroup: sidebar tab navigation actionsRightIconGroup: utility and status actions
Control mapping:
- Left group:
- Tasks tab (
SHIFT + 1) dispatchesviewSlice.actions.updateSidebarTab("tasks") - Month tab (
SHIFT + 2) dispatchesviewSlice.actions.updateSidebarTab("monthWidget")
- Tasks tab (
- Right group:
- Command palette toggle (
modifier + K) dispatches open/close palette actions fromsettingsSlice - Google status action is derived from
useConnectGoogle().sidebarStatus - background import spinner is shown while
selectImportGCalState(...).importingis true - update action (refresh icon) is shown when
useVersionCheck().isUpdateAvailableis true and reloads the page
- Command palette toggle (
Icon state constraints:
- tab icons and command icon use
theme.color.text.lightwhen active andtheme.color.text.darkPlaceholderwhen inactive - Google status icon tooltips and disabled/clickable behavior come from
useConnectGoogleand should not be hardcoded in the row component
Dedication Dialog Runtime
Files:
packages/web/src/views/Calendar/components/Dedication/Dedication.tsxpackages/web/src/views/Calendar/Calendar.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)
Day/Now Shortcuts Sidebar Runtime
Files:
packages/web/src/common/hooks/useSidebarState.tspackages/web/src/views/Day/components/ShortcutsSidebar/ShortcutsSidebar.tsxpackages/web/src/views/Day/components/Header/Header.tsxpackages/web/src/views/Day/view/DayViewContent.tsxpackages/web/src/views/Now/view/NowView.tsxpackages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.tspackages/web/src/views/Now/shortcuts/useNowShortcuts.ts
Runtime behavior:
- sidebar state is responsive-first:
useSidebarStatesets open/closed fromwindow.innerWidth >= 1280(xl) and subscribes tomatchMedia("(min-width: 1280px)") - breakpoint transitions are authoritative: crossing the
xlboundary re-syncs the sidebar state even if the user manually toggled it earlier - users can toggle via:
- header sidebar button (
Headertooltip +SidebarIcon) [keyboard shortcut in both Day and Now views
- header sidebar button (
- the sidebar is desktop-only in layout (
hidden xl:flex), so on sub-xlwidths toggling updates state but the sidebar content remains visually hidden ShortcutsSidebarfilters out empty sections and returnsnullwhen no section has shortcuts
Animation and visibility contract:
- opening uses
requestAnimationFrameto set visible state so entry transition classes apply (translate-x-0 opacity-100) - closing sets hidden classes (
-translate-x-4 opacity-0) and unmounts when closed/not visible
Pitfalls:
useSidebarStatereadswindowduring state initialization and useswindow.matchMedia; browser-like globals must exist in tests/non-browser runtimes- when adding sidebar-driven interactions, verify both Day and Now routes to keep keyboard behavior aligned (
[should work in both)
State Systems
The web app uses multiple state layers:
| Concern | Use | Key files |
|---|---|---|
| Loading states, modal visibility, async status | Redux Toolkit slices | packages/web/src/ducks/events/slices/ |
| Async sequences and persistence orchestration | redux-saga | packages/web/src/ducks/events/sagas/event.sagas.ts |
| Event entity CRUD, active event, and draft state | Elf store | packages/web/src/store/events.ts |
| Offline persistence | IndexedDB adapter | packages/web/src/common/storage/adapter/indexeddb.adapter.ts |
| Local vs remote persistence choice | Repository factory | packages/web/src/common/repositories/event/event.repository.util.ts |
These layers are intentional. Do not collapse event entities into Redux slices or call IndexedDB directly from components.
Read these together for event work:
packages/web/src/store/index.tspackages/web/src/store/sagas.tspackages/web/src/ducks/events/sagaspackages/web/src/store/events.ts
Event Flow
Typical event flow:
- a route view, hook, or component dispatches a Redux action
- redux-saga handles the async side effect
- the selected repository writes locally or remotely
- the saga updates the Elf event store
- Redux slices update async status
- React re-renders from observables/selectors
- SSE events can trigger refetch or metadata refresh later
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:
- event behavior is not owned by a single state system
- when debugging, inspect the action, saga, repository, and store layer together
Styling Systems
The web app currently uses two styling systems in parallel:
- longstanding
styled-componentsfor much of the existing UI - Tailwind v4 utilities and semantic theme tokens from
packages/web/src/index.cssfor newer or migrated surfaces
Do not describe the frontend as Tailwind-only or styled-components-only. Follow the local pattern of the area you are editing unless the change is explicitly migrating that area.
Day Task Drag Handle Positioning
File:
packages/web/src/views/Day/components/Task/DraggableTask.tsx
DraggableTask uses @floating-ui/react to place the reorder handle. The component explicitly strips non-finite floating coordinates (left/top) before applying styles. This avoids invalid inline styles when the layout engine cannot resolve a finite position and keeps task rows render-safe during drag-handle visibility transitions.
Repository Selection
File:
packages/web/src/common/repositories/event/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/adapter/adapter.tspackages/web/src/common/storage/migrations/migrations.ts
Startup storage flow:
- create or reuse the storage adapter 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,SOMEDAY_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/hooks/google/useConnectGoogle/useConnectGoogle.tspackages/web/src/auth/google/google.auth.util.tspackages/web/src/views/Calendar/components/Sidebar/SidebarIconRow/SidebarIconRow.tsx
UI state comes from a single server-enriched metadata field (google.connectionState) plus one client-only loading state:
checking(client-only) → disabled checking status (SpinnerIcon)NOT_CONNECTED→ connect action (CloudArrowUpIcon)RECONNECT_REQUIRED→ reconnect action (LinkBreakIcon)IMPORTING→ disabled syncing status (SpinnerIcon)HEALTHY→ disabled connected status (LinkIcon)ATTENTION→ repair action (CloudWarningIcon)
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 SSE hooks, sync slice, event sagas.
- Offline issue: read storage adapter and migration runner.
- Rendering issue in day/week/now: start at the route view, then its hooks.