Architecture
Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page
Status: Accepted
How the system is put together, and why each structural choice is the one the requirements demand.
The rule this section is held to
Every architectural decision here cites a requirement it satisfies. A technical choice justified by nothing is unjustified, and should be challenged in review.
This is the practical form of writing the functional spec first: architecture is written against 10-functional, not alongside it. Where a decision has no requirement behind it, one of two things is true — the requirement is missing, or the decision is unnecessary. Both are worth finding.
Contents
| Doc | Covers |
|---|---|
| system-context.md | What’s inside, what’s outside, trust zones, what crosses the boundary |
| component-model.md | Crate layout, the core/UI boundary, Docker access split, async model |
| data-flow.md | Content pipeline, control flow, observation, seeding |
| platform-matrix.md | The five cross-platform differences that actually bite |
| contracts/stack-manifest.md | stack.toml — the lemonfiber ↔ lemonfiber-media-stack interface, full schema |
| contracts/plugin-manifest.md | plugin.toml — the lemonfiber ↔ plugin interface, and the Compose entry lemonfiber writes from it |
| contracts/plugin-manifest-service.md | plugin.toml [[service]] — each service a plugin runs, field by field |
| contracts/plugin-manifest-proof.md | plugin.toml [[proof]] — what must hold before a plugin is installed |
| contracts/plugin-manifest-recipe.md | plugin.toml [[recipe]] — the ordered calls a plugin may declare, bounded by reading |
| contracts/plugin-manifest-secret.md | plugin.toml [[secret]] and [[override]] — the secrets a plugin holds, minted ones included, and the bundled things it changes |
| contracts/capability-vocabulary.md | capability-vocabulary.json — what a service can do, what demonstrates it, and who may claim one |
| contracts/extension-points.md | extension-points.json — where a plugin may add a row to a register lemonfiber already runs |
| contracts/download-client.md | Registering SABnzbd/qBittorrent into a Servarr app — the fields schema and per-app category |
| contracts/prowlarr-application.md | Registering a Servarr *arr as a Prowlarr application — the /api/v1 fields schema and per-media sync categories |
| contracts/jellyfin-seerr-identity.md | Making Jellyfin Seerr’s identity source — driving Jellyfin’s first-run setup and Seerr’s auth/jellyfin init |
| contracts/certificates.md | The certificate a program serves beyond its machine: operator files, ACME, a lemonfiber-made private root, or a pinned certificate of its own |
| contracts/versioning.md | Three version identifiers, compatibility, where skew is caught |
| contracts/web-api.md | What the web surface may ask for, how live state arrives, what guards it |
| contracts/web-api-artefact.md | The web API artefact — the generated contract, the refusals it lists, and how it reaches an SDK |
| contracts/web-api-getting-in.md | Getting in to the web API — the per-run token, sessions, integration keys and the limit wrong answers meet |
| contracts/web-api-wire.md | The web API on the wire — the token’s header, the address, the heartbeat, resumption, a job’s outcome, an action sent again, the payload’s type and how an instant is written |
| contracts/design-tokens.md | The brand → lemonfiber-web interface — the visual language as data the app consumes |
The ARCH-R namespace
Architectural requirements use ARCH-R##, alongside feature requirements
(A2-R4), governance rules (GOV-R12) and quality rules (Q-R3).
They exist because some obligations are structural rather than behavioural.
“lemonfiber-core must not depend on any UI crate” is not something a user
observes — but it’s what makes “surfaces are renderings, never capabilities”
(G1-R2) true rather than
merely intended.
The four structural decisions
Everything else follows from these:
1. Compose is the execution engine
Profiles are a Compose concept, and partial stacks are the product’s core proposition. Reimplementing them against the raw Docker API would mean reimplementing Compose — and would break the guarantee that the stack runs without lemonfiber at all. → ADR-0001
2. Reads and writes take different paths
Writes go through docker compose because profiles live there. Reads go through
the Docker API because a 1 Hz dashboard across 20 services cannot afford process
spawns. Neither path can serve both jobs.
→ ADR-0008
3. The stack is data, not code
Services, profiles and forms are declared in stack.toml. Adding a service is a
data change with no Rust edit and no release — which is also what makes
third-party stacks possible.
→ ADR-0002 ·
stack-manifest
4. Logic cannot render
lemonfiber-core has no UI dependency of any kind. A surface cannot grow
behaviour of its own, because behaviour lives somewhere that cannot print.
→ component-model
Reading order
- Implementing across the seam → stack-manifest → versioning
- Working inside
lemonfiber→ component-model → data-flow - Chasing a platform bug → platform-matrix
- Orienting → system-context
Related
- 00-overview/decisions — the contested choices
- 30-repos — per-repo detail below this level
- 40-quality — how the code is written