← The specification

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