Skip to content

Architecture

Layered architecture and the dependency rule Four layers: entrypoints and adapters on the outside, application in the middle, and a pure domain at the centre. Dependencies point inward only. The domain imports nothing internal, the application imports only the domain, and adapters implement the ports the application declares. entrypoints/ CLI · Fastify API · shared wiring adapters/ collectors · evaluators · notifiers · repositories · auth · key pools application/ ports · the run loop · due-filtering · per-watch lock domain/ Zod models · decision rules · zero I/O The rule Dependencies point inward. domain imports nothing internal. application imports only domain. adapters implement ports. Why it matters The same core runs as a scheduled CLI job and as an always-on service, unchanged. Decision logic is pure, so it is tested without network or keys.
Dependencies point inward. Nothing in the middle knows what is outside it.

domain/ imports nothing internal. application/ imports only domain/. adapters/ and entrypoints/ may import anything.

Zod models and decision rules. No I/O, and no reading the clock: time arrives as an argument, so every function is a deterministic function of its inputs.

This is where “what counts as new”, “is this quote real”, “may this fire” and “is this watch due” live.

The processing loop, due-filtering, the per-watch lock, and the port interfaces the loop depends on. It imports the domain and nothing else, and it performs no I/O of its own — it calls ports.

Every implementation of every port: collectors, evaluators, embedders, notifiers, repositories, the mailer, auth, key pools and validation probes. If something touches the network, the disk or the clock, it belongs here.

Two runtimes over one core:

Entrypoint What it is
CLI run-due plus watch and user management commands
API A Fastify app plus an in-process scheduler
Shared wiring The single place the real adapter graph is assembled

Both call the same wiring, which is what stops them drifting apart.

Source → Collector → Candidate[] → (pure delta) → Evaluator → EvaluationResult
→ (pure validation + tiering) → Notification → Notifier
→ (pure state update) → Repository

Every arrow crossing into the outside world is a port; every transformation between them is pure.

Zod schemas guard every external boundary: collector output, model output, YAML watch files, HTTP request bodies, and every document read from the database. A malformed item is rejected at the edge rather than propagating inward, and the inferred types are the single source of truth used across all four layers.

The monitoring is a short-lived outbound job — perfect for a scheduled runner and free of hosting cost. The API is a long-lived service that only exists for management. Keeping the core independent of both means neither constrains the other, and either can be replaced without touching decision logic.