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

95 lines
4.2 KiB
Markdown

# 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
```text
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](deployment.md#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](metrics.md) 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](development.md). For running it and backing it
up, see [deployment](deployment.md).