← The specification

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.