Common Change Recipes
These are the safest implementation paths for common Compass changes.
Add A Backend Endpoint
- Define or extend a shared schema/type in
packages/core/src/typesif the contract is shared. - Add the route to the relevant
packages/backend/src/*/*.routes.config.ts. - Keep the controller thin in
controllers/*.controller.ts. - Put business logic in
services/*.service.ts. - Add controller or service tests.
- If the endpoint affects realtime UI, decide whether an SSE event is required.
Add A New Event Field
- Update the event schema/types in
packages/core/src/types/event.contracts.ts. - Update any mapper or utility code in
packages/core/src/mappersorpackages/core/src/util/event. - Update backend persistence or parser logic if the field is stored or transformed.
- Update web editors, selectors, and rendering.
- Add tests in
core,web, andbackendas needed.
Rule: never treat event shape as web-only unless the field is strictly presentational.
Change Recurring Event Behavior
- Read
packages/core/src/types/event.contracts.ts. - Read
docs/features/recurring-events-handling.md. - Read
packages/backend/src/event/classes/compass.event.generator.ts. - Read
packages/backend/src/event/classes/compass.event.parser.ts. - Read
packages/backend/src/event/classes/compass.event.executor.ts. - Read
packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation.ts. - Update the planner, executor, or scope-expansion path that actually owns the behavior.
- Add focused tests for the exact recurrence transition you changed.
Do not edit recurring behavior from one layer only.
Common Mistakes
- Changing only the generator/executor without updating the parser's plan —
analyzeReplace(...)andanalyzeDelete(...)incompass.event.parser.tsare pure functions that turn a target event, itsSeriesContext(base + instances), and the mutation input into aReplacePlanorDeletePlan(replaceThis/replaceSeries/replaceSplit,deleteThis/deleteSeries/deleteSplit).compass.event.generator.ts(generateReplace/generateDelete) expands that plan into concrete records to persist (upsert,deleteIds,primary), andcompass.event.executor.ts(executeMutation/executeDelete) is the only layer that touches the DB. If the parser produces the wrong plan kind or wrong instance ids, the generator and executor will faithfully do the wrong thing even though their own logic is correct. Always trace fromEventService.replace/EventService.deleteinpackages/backend/src/event/services/event.service.tsthroughanalyzeReplace/analyzeDeletefirst. - Missing a database migration for existing recurring events — existing user data will not be retroactively updated by code changes alone. If you modify how recurring series are stored or processed, add a migration to
packages/scripts/src/migrations. - Testing only the happy-path transition — cancellation transitions follow a different planner path. A test that only covers the primary create/update flow can pass while cancellation transitions break silently.
Triage A Recurrence Sync Regression
- Reproduce with one event and one expected outcome (which scope:
this,thisAndFollowing, orall). - Trace the request through
EventService.replace/EventService.deleteinpackages/backend/src/event/services/event.service.ts: confirmseriesContext(...)resolved the rightbase/instances, then log or debug theReplacePlan/DeletePlanreturned byanalyzeReplace/analyzeDeleteinpackages/backend/src/event/classes/compass.event.parser.ts— check itskindand, for split plans, thedeleteInstanceIdsandtruncatedBase/newBase. - Confirm
generateReplace/generateDeleteinpackages/backend/src/event/classes/compass.event.generator.tsexpand that plan into the expectedupsert/deleteIds/primary(ordeleteSeriesId). - Confirm
executeMutation/executeDeleteinpackages/backend/src/event/classes/compass.event.executor.tsapply those records viaeventRepository. - If the DB side is correct but Google is wrong, check
CompassToGoogleEventPropagation.propagate(...)inpackages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation.ts— it logsCompass->Google propagation failedon error andSkipping Google effect for user <id> because Google is not connected.when there's no refresh token; checkdeletedBefore/originalStartByEventIdpassed in fromevent.service.ts. - Run focused tests:
bun run test:backend --runTestsByPath packages/backend/src/event/classes/compass.event.parser.test.ts packages/backend/src/event/classes/compass.event.generator.test.ts packages/backend/src/event/classes/compass.event.executor.test.ts packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google.event-propagation.test.ts --runInBand
Add An SSE Event
- Add the event name to
packages/core/src/constants/sse.constants.ts. - Update shared payload types in
packages/core/src/types/sse.types.tsif needed. - Emit from
packages/backend/src/servers/sse/sse.server.ts(or call site that usespublish). - Consume it in a web hook under
packages/web/src/sse/hooks(listeners onEventSource). - Add tests on both emitter and listener sides.
Add Or Change Local Storage Data
- Update
packages/web/src/common/storage/offline-data/offline-data.store.tsif the public store contract changes. - Update
packages/web/src/common/storage/offline-data/indexeddb-offline-data.store.ts. - Add a migration if existing user data could become invalid.
- Add offline data store and migration tests.
Common Mistakes
- Adding new fields without a migration — existing users already have data in IndexedDB without the new field. If your code expects the field to be present, it will fail silently or throw on their existing records. Always add a migration in
packages/web/src/common/storage/migrations/migrations.tsand test the migration path, not just the new code path. - Testing only the new code path — write a test that starts with pre-migration data (the old shape) and confirms the migration transforms it correctly. A test that only creates fresh data will not catch migration regressions.
Change Repository Selection Or Offline Behavior
- Start in
packages/web/src/events/repositories/event.repository.util.ts. - Verify auth-state implications in
packages/web/src/auth/session/SessionProvider.tsxand auth-state helpers. - Test both never-authenticated and previously-authenticated behavior.
Change A Shared Hotkey Dialog (Day + Week)
Use this for overlays mounted in both WeekView and DayViewContent (for example Dedication).
- Update the shared dialog component in
packages/web/src/views/Week/components/Dedication/Dedication.tsx. - Confirm both mount points still render it:
packages/web/src/views/Week/WeekView.tsxpackages/web/src/views/Day/view/DayViewContent.tsx
- Keep keyboard behavior aligned:
- toggle hotkey (
ctrl+shift+0) - close hotkey (
escapewhen open)
- toggle hotkey (
- Preserve the transition lifecycle:
- open with
showModal()then set visible state - close by state first, then
dialog.close()inonTransitionEnd - keep
onCancel(e.preventDefault())so Escape uses the animated close path
- open with
Common pitfall: calling dialog.close() directly in an event handler will skip the CSS exit transition and can produce abrupt UI changes.
Add A Migration
For database migrations:
- inspect
packages/scripts/src/commands/migrate.ts - add migration under
packages/scripts/src/migrations - run the relevant scripts tests
For web local-data migrations:
- inspect
packages/web/src/common/storage/migrations/migrations.ts - add the migration to the correct registry
- add migration tests
Change Environment Handling
- Update the relevant env schema:
- backend:
packages/backend/src/common/constants/env.constants.ts - web:
packages/web/src/common/constants/env.constants.ts
- backend:
- Confirm startup behavior still works in the intended dev mode.
- Document any new required variables.
Type A Hook That Accepts A queryOptions-Builder Function
Some web hooks take a TanStack Query queryOptions(...)-returning function as
a parameter (for example usePrefetchAdjacentEvents, which takes either
weekEventsQueryOptions or dayEventsQueryOptions in
packages/web/src/events/queries/usePrefetchAdjacentEvents.ts) so the same
hook works for either view. Two approaches that look reasonable both fail to
type-check:
- A named function-type alias with a fixed return shape (e.g.
(args) => FetchQueryOptions<never, Error, never>) —never/unknownerase the concretequeryKeytuple type each call site actually returns, producing'queryKey' requires 3 elements but source may have fewererrors. - A union of the concrete function types
(
typeof weekEventsQueryOptions | typeof dayEventsQueryOptions) — calling a union of functions collapses the return type in a way that fails to unify with the consumer's own generic inference for one of the two shapes.
What works: make the consuming hook itself generic with the same type
parameters prefetchQuery/useQuery use
(TQueryFnData, TError, TData, TQueryKey extends readonly unknown[]), and
type the parameter as
(args: EventsQueryArgs) => FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>.
Each call site then independently instantiates the generic via inference —
no union collapsing, no erased tuple type. TanStack's generic surface is
designed for per-call-site inference; piggyback on that instead of fighting
it with a shared named type.
Add A New CLI Command
- Register the command in
packages/scripts/src/cli.ts. - Implement behavior in
packages/scripts/src/commands. - Reuse shared CLI utilities from
packages/scripts/src/common. - Add integration tests in
packages/scripts/src/__tests__.