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
95 lines
4.2 KiB
Markdown
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).
|