Customisation & escape hatches
Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page
Status: Accepted · Audience: Operator · Area: F — Extensibility
Purpose
Let an experienced operator change anything, and guarantee that lemonfiber is never the thing standing between them and their stack.
This audience matters disproportionately. They’re the ones who evaluate the project publicly, who answer questions in forums, and whose objection — “it hides what’s actually happening” — kills adoption among exactly the people newcomers ask for advice.
They are also right to be suspicious. A tool that wraps a system in an opaque layer, then breaks or is abandoned, leaves its users with something they cannot operate.
Behaviour
The stack runs without lemonfiber
The project lemonfiber writes is a real Compose project. It names itself, writes
every value it runs with into itself, and keeps beside it a .env naming the
profiles of the last start and the files to merge: the project, then the operator’s
overlay where there is one. Run docker compose up in the project’s directory and
the stack lemonfiber last started comes up, with no lemonfiber binary running
(ADR-0001).
This is the load-bearing guarantee. Everything else in this feature is convenience; this is the property that makes adopting lemonfiber a low-risk decision, because abandoning it is always possible.
Every command shows its work
--dry-run prints the exact underlying invocation without executing it. Nothing
is generated that the operator cannot read.
A local stack can be substituted wholesale
--stack-dir points at a fork (ADR-0005).
The operator maintains their own compose file and manifest; lemonfiber operates
it, validating only the manifest contract.
Materialised files are editable and respected
Files lemonfiber writes may be edited directly. Modifications are detected by content and never silently overwritten on upgrade — the operator is shown a diff and chooses (C9).
Adding a service requires no Rust
A new service is a compose entry plus a manifest entry plus inclusion in whichever forms should carry it. No lemonfiber change, no release (ADR-0002).
Everything interactive is also non-interactive
Every action available in the TUI or web UI has a flag-driven equivalent that runs unattended and exits with a meaningful status. This is what makes lemonfiber scriptable, and what stops the friendly interface from becoming a cage.
Opting out of a managed area is supported
An operator can declare a service, or a specific configuration area, unmanaged. lemonfiber then reports its state but never writes to it — and stops reporting drift for it.
On the companion
This feature’s guarantee is the one a graphical surface loses most easily, and an app is the most graphical surface this product has: no shell to fall back to, no flags to pass, no obvious place to read what is about to happen. If the escape hatch is not visible from the app then for anybody whose main surface is their phone it does not exist (N19).
Every command shows its work becomes N19-R1: where the app offers an action
it can show the command that performs it, without the operator having to reach
another surface to find out. N19-R7 carries the rest — an operator wondering
what happens if they delete the app should be able to find that out from inside
it.
N19-R2 is small and load-bearing. A route offered with a caution that
discourages taking it has been closed politely, and the audience this feature
is written for reads that correctly the first time. Their objection — “it hides
what’s actually happening” — is about exactly the shape an app naturally takes.
Materialised files are editable and respected and opting out of a managed
area is supported become N19-R3 and N19-R5, and they fail the same way. An
edited file and a drifted one are opposite facts about the same bytes; an
unmanaged area and an unconfigured one are opposite facts about the same
silence. A surface reading only a checksum, or only a count of what is
configured, cannot tell a decision from a defect — it renders the decision as
the defect and then offers to fix it, which is asking the operator to undo
something they chose on purpose.
States
Per managed area:
| State | Meaning |
|---|---|
managed |
lemonfiber maintains it |
modified |
Locally edited; preserved, reported |
unmanaged |
Operator opted out; observed only |
forked |
Operating from an external stack directory |
contract-invalid |
External stack fails manifest validation |
Edge cases
| Situation | Behaviour |
|---|---|
| Forked stack fails manifest validation | Refuse with the specific violation and location named — not “invalid manifest”. |
| Forked stack uses an unsupported schema version | Refuse, naming both versions (versioning contract). |
| Operator adds a service lemonfiber doesn’t know | Lifecycle and status work generically. Features needing specific knowledge — seeding, queue health — report as unsupported for that service rather than failing. |
| Operator removes a service lemonfiber depends on | Report which features become unavailable; don’t refuse. |
| Edited file conflicts with an upgrade | Show a diff; the operator chooses. Never auto-merge. |
| Operator wants lemonfiber to stop managing everything | Supported — a fully unmanaged stack that lemonfiber only observes. |
| Fork diverges far from the manifest contract | Validation reports every violation at once, not one per run. |
| Operator scripting against output | Machine-readable output is a stable interface and MUST be versioned. |
| Compose file edited to break the single-mount rule | Report the consequence (C5); do not refuse. It’s their system. |
| Operator wants to run two stacks on one host | Not supported: one machine runs one lemonfiber stack. Each network’s subnet is derived from its name alone, so a second project names the subnets the first holds and the engine refuses its networks (ARCH-R310). |
Acceptance criteria
| ID | Requirement |
|---|---|
| F1-R1 | The stack MUST be fully operable with plain docker compose in the project’s directory, from the project lemonfiber writes, the files it names and the .env beside it, with no lemonfiber binary running. |
| F1-R2 | Every command MUST support a dry-run mode printing the exact underlying invocation. |
| F1-R3 | An external stack directory MUST be usable in place of the embedded stack. |
| F1-R4 | Locally modified materialised files MUST be detected by content and MUST NOT be overwritten without a diff and confirmation. |
| F1-R5 | Adding a service to the stack MUST NOT require a lemonfiber code change or release. |
| F1-R6 | Every interactive action MUST have a non-interactive equivalent with a meaningful exit status. |
| F1-R7 | Services and configuration areas MUST be markable as unmanaged, after which lemonfiber MUST observe but never write. |
| F1-R8 | Unmanaged areas MUST NOT be reported as drift. |
| F1-R9 | Manifest validation failures MUST name the specific violation and its location, and MUST report all violations in one pass. |
| F1-R10 | An unknown service MUST still support generic lifecycle and status operations. |
| F1-R11 | Features requiring service-specific knowledge MUST report unsupported for unknown services rather than failing. |
| F1-R12 | Machine-readable output MUST be a versioned, stable interface. |
| F1-R13 | A configuration violating internal guidance MUST be reported with its consequence, and MUST NOT be refused. |
| F1-R14 | Multiple independent stacks MUST be supportable on one host, with port conflicts detected. |