Files
when/docs/architecture.md
eleith d9aba2d6cd
ci/woodpecker/pr/calendar Pipeline was successful
ci/woodpecker/pr/cli Pipeline was successful
ci/woodpecker/pr/config Pipeline was successful
ci/woodpecker/pr/db Pipeline was successful
ci/woodpecker/pr/jobs Pipeline was successful
ci/woodpecker/pr/video-chat Pipeline was successful
ci/woodpecker/pr/web Pipeline was successful
ci/woodpecker/pr/worker Pipeline was successful
ci/woodpecker/push/calendar Pipeline was successful
ci/woodpecker/push/cli Pipeline was successful
ci/woodpecker/push/config Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/jobs Pipeline was successful
ci/woodpecker/push/video-chat Pipeline was successful
ci/woodpecker/push/web Pipeline was successful
ci/woodpecker/push/worker Pipeline was successful
refactor(config): migrate app reload lifecycles
2026-11-02 10:14:34 -08:00

4.2 KiB

Architecture

One booking app. Two processes. Shared files.

This is the map. The code has the street names.

The pieces

Node 26, strict TypeScript and pnpm workspaces. SvelteKit serves web; node:sqlite and Kysely handle data. OpenWorkflow runs background jobs.

Path Job
apps/web Booking, admin and HTTP routes.
apps/worker Calendar I/O, video links and email.
apps/cli Config validation and operator commands.
packages/config TypeBox schema, loading and reload monitoring.
packages/db App database, queries and migrations.
packages/jobs Workflow contracts and queue client.
packages/calendar Google/CalDAV adapters and local ICS helpers.
packages/video-chat Google Meet and Nextcloud Talk adapters.

Configuration

The config package validates When's YAML and provides a schema-independent reload monitor. Each process selects its loading context once. The monitor serializes reads and tracks the last accepted value; the app decides whether to apply an edit or restart boot-time resources. Invalid edits keep the accepted configuration.

The monitor returns cleanup before reconciling startup changes, so shutdown owns it immediately. Web uses SvelteKit's shutdown hook; worker keeps its normal resource shutdown order. There is no separate package-global configuration cache.

A booking

request → web: validate → save appointment → enqueue job → respond
                                               ↓
                       worker: calendar / video / email → record outcome

Web and worker share when.yaml, when.sqlite and openworkflow.sqlite. Web imports job contracts, not worker implementations. Producers do not start a worker or register schedules.

Booking reads a local busy-time mirror. Calendar providers expand recurrence; CalDAV must support CALDAV:expand. Failed imports keep the old mirror, which is not proof it is current. Calendar publishing writes content; video adapters handle conferencing. Those network calls stay in the worker.

Background work

The worker has one execution slot. Active slow calls block other jobs; native retry delays park and release the slot. Saved steps avoid repeating checkpointed successes, but a crash before checkpointing can duplicate a remote effect. At-least-once, not exactly-once.

Enqueue keys deduplicate for 24 hours, including terminal runs. They are not retry commands. Worker stop is not queue draining. See updates before deploying changed workflow graphs.

Instrumentation

Prometheus code lives in each app's metrics area. Public appointment APIs and worker service facades wire the observers. HTTP routes explicitly record known outcomes through those metrics modules; auth uses its native hooks. Domain implementations do not import Prometheus or record metrics. See metrics for what the counters mean.

Time

Use native Temporal on the server and temporal-polyfill/fns/* in the browser. Inject a clock into domain logic.

Displayed times follow the context's stored zone where applicable, then the tz cookie, then the browser on first visit. Until the zone is known, show blank space or a skeleton, not guessed times. preferredTimezone.svelte.ts owns the preference.

Slot links carry an absolute instant; day links carry a date in the viewer's zone. There is no tz query parameter. Duration links must name an allowed choice; otherwise use the configured default or the current booking's reschedule length.

Styling and theming (web)

Scoped CSS and Bits UI. Use theme.css tokens for colors, dimensions, type, radii, shadows and transitions; no hardcoded colors or pixel dimensions. Components use the shared light/dark palette, not local dark-mode patches.

Keep component copy in markup. Script is for behavior, not identifier-to-sentence mappers. A data-derived value for genuinely repeated copy is fine.

For development, see development. For running it and backing it up, see deployment.