← The specification
Architecture Decision Records
Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page
Status: Accepted
An ADR captures a decision that was contested — where a reasonable engineer could have chosen otherwise. It records the alternatives and why they lost, so that revisiting the decision later starts from evidence rather than from scratch.
Rules
- ADRs are immutable once Accepted. To change a decision, write a new ADR that supersedes the old one and link both ways. The record of changing your mind is the valuable part.
- Only genuinely contested decisions get an ADR. “We use
serdefor JSON” is not a decision, it’s a default. - Numbers are permanent and never reused.
Index
| # | Decision | Status |
|---|---|---|
| 0001 | Docker Compose as the execution engine | Accepted |
| 0002 | Profiles are facts; forms are intent | Accepted |
| 0003 | Rust + Ratatui for the CLI/TUI | Accepted |
| 0004 | Four repos rather than a monorepo | Accepted |
| 0005 | lemonfiber embeds the stack at build time | Accepted |
| 0006 | One /data mount, subdirectories beneath |
Accepted |
| 0007 | Jellyfin supports both Docker and native | Accepted |
| 0008 | Compose CLI for writes, Docker API for reads | Accepted |
| 0009 | Pin every action to a SHA, including our own reusables | Accepted |
| 0010 | A container engine is a v2 detail, not a v1 assumption | Proposed |
| 0011 | The web surface is a fifth repo behind the JSON contract | Proposed |
| 0012 | The web surface ships inside the binary, as a pinned submodule | Proposed |
| 0013 | The API gets an SDK, and the SDK gets its own repo | Proposed |
| 0014 | Every SDK generates its types from one artefact the server emits | Proposed |
| 0015 | The documentation site renders content it does not own | Proposed |
| 0016 | Dependabot updates the dependencies, and the gate cites for it | Proposed |
| 0017 | The companion app is a fourth surface, reached over the network | Accepted |
| 0018 | A paired fingerprint decides which machine the app will talk to | Accepted |
| 0019 | A screen paints what it knows before it reaches the stack | Accepted |
| 0020 | An action the stack did not receive did not happen | Proposed |
| 0021 | A plugin is data, and lemonfiber writes its container | Proposed |
| 0022 | A recipe declares pairs, not lists | Proposed |
| 0023 | A pin is a digest, and a signature is a different question | Proposed |
| 0024 | Data about this installation opens; behaviour lemonfiber implements does not | Proposed |
| 0025 | A pin is the certificate’s digest, and it is what permits an address off this machine | Accepted |
| 0026 | A screen may hold the event stream, and holding it is its one read | Proposed |
| 0027 | A member plays what the core authorised, on the member’s own account | Accepted |
| 0028 | A supported major is data the stack declares, proved by its own recordings | Accepted |
| 0029 | A household service declines an invitation, holding one media-server key and nothing else | Accepted |
| 0030 | An assertion declared to fail on a recording is reported as failing as declared | Accepted |
| 0031 | Installing a plugin and choosing a filler are web API writes; replacing a credential is not | Accepted |
| 0032 | The request service reaches the *arrs and the media server through a gate, holding only the gate’s tokens | Accepted |
| 0033 | Each image lemonfiber builds for the stack has its own repository, named for the service it runs | Accepted |
| 0034 | A catalogue release is signed with a key pair, and lemonfiber carries the public half | Accepted |
| 0035 | What the phone keeps, and how | Accepted |
| 0036 | A plugin may come from any source the command takes, over the web API too | Accepted |
| 0037 | An integration key is a credential the web API may mint, where the operator proves themself again | Accepted |
| 0038 | lemonfiber ships as an image that holds the host’s Docker | Accepted |
| 0039 | Each repository records what it built, one row per requirement | Accepted |
| 0040 | Three sites, each for one reader, and the specification on the frontpage | Accepted |
| 0041 | Every part is a plugin, and the core speaks contracts | Accepted |
| 0042 | The bundle is first-party plugins, pinned and embedded | Accepted |
| 0043 | The core writes every Compose entry from one typed service model | Accepted |
| 0044 | A phone proves the operator with a device key, so it never holds the password | Accepted |
| 0045 | Every bundled part has its repository now, and its adapter later | Accepted |
| 0046 | The stores are filled once, at the cutover, and a secret never enters the journal | Accepted |
| 0047 | A phone hears its stacks without a vendor’s push service, in the kinds the core declares | Accepted |
| 0048 | A part without a port is proved by running its own program | Accepted |
| 0049 | A moved pin is recorded again by the core | Accepted |
| 0050 | A contract hands its filler all it enforces, and reads back no secret | Accepted |
| 0051 | A fresh claimant answers its probe, and the core asks what nothing else does | Accepted |
| 0052 | A curator’s downloads complete into one folder, and the core tells each part what it holds | Accepted |
| 0053 | A part claimed first is claimed over a contract, and an indexer source is pulled over its own | Accepted |
Not here: licensing. It’s a project-governance choice, not an architectural one — no component’s design depends on it. See licence rationale.
Template
# ADR-NNNN: <title>
**Status:** Proposed | Accepted | Superseded by ADR-NNNN
**Date:** YYYY-MM-DD
## Context
What forces are at play? What makes this non-obvious?
## Decision
What we're doing, stated plainly.
## Alternatives considered
| Option | Why it lost |
## Consequences
### Positive
### Negative
### Neutral
## Revisit if
Concrete conditions that would justify a new ADR.