The envelope
Every machine-readable answer arrives in the same wrapper. There is no second shape for a second surface: a script piping a command’s output and a client fetching the equivalent endpoint receive the same document.
{ "api_version": 1, "kind": "walkthrough", "data": {} }| Field | Type | Required | What it is |
|---|---|---|---|
api_version | integeruint32, ≥ 0 | yes | The output contract's version. |
data | the payload | yes | The payload. |
host | stringornull | no | The machine this answer is about, where it is not the one lemonfiber runs on. |
job | stringornull | no | The work that said this, by the name its accepting reply gave it. |
kind | string | yes | Which payload this is, so a consumer can branch before parsing |
api_version, kind and data are always there; host and job only where
they apply. kind comes before data in importance rather than in
byte order: a client reads it first and then reads data as that kind, never
as an open value. The kinds and their payloads are
every payload kind.
On the command line
Section titled “On the command line”--json on any command. The answer goes to standard output; a failure is an
envelope too, of kind error, and it goes to the error stream instead — so a
script reading standard output never has to tell an answer from an apology.
A log stream is the one shape that is not a single document. It emits one envelope per line, because a stream has no last element to close a document with.
The plain-language footnote a human report prints under itself is never appended to a machine-readable answer. It is prose for a person, and adding it would corrupt the one thing that answer exists to be.
The HTTP surface
Section titled “The HTTP surface”lemonfiber ui serves the same envelope over local HTTP. It takes a loopback
socket — whichever port is free, unless --port names one — and prints the whole
address together with a token minted for that run. --lan offers it to your
network instead, and is refused until a password has been set with
--set-password. Nothing is installed, and nothing keeps running afterwards. The
connection is plain text unless --tls asks for it encrypted, with a certificate
lemonfiber made and keeps, and it says which as it starts. --tls needs --port,
because it is for a paired phone, and a phone keeps the address it was given.
Thirty-eight endpoints answer a question and close. Each one is a command a person could have typed, dispatched through the same entry point the command line uses, so the two surfaces cannot say different things about the same stack.
| Endpoint | What it answers |
|---|---|
GET /api/status |
What the whole stack is doing |
GET /api/services |
The same reading, narrowed to the forms named in ?form= |
GET /api/forms |
Every form the stack declares, or what the ones named in ?form= would come to |
GET /api/checks |
What the diagnostic checks found, or the one group named in ?only= |
GET /api/storage |
The checks about the disk |
GET /api/logs |
The scrollback, one envelope per line; takes ?form=, ?service= and ?tail=, and ?follow answers with a job name and carries the lines on the stream |
GET /api/requests |
What the household has asked for, narrowed to ?member= — and always to themselves when a member is asking |
GET /api/held |
What one member can actually watch, as the media server answers it for them. ?member= is required; ?most= defaults to 100 and is refused, not trimmed, above 500 |
GET /api/trace |
Where one item got to; ?term= names it as you would say it, ?season= narrows to one |
GET /api/stuck |
The items whose downloads have stopped, each named the way ?term= asks for one |
GET /api/config |
Every setting, credentials withheld, or the one named in ?key= |
GET /api/quality |
The preset in force, what each preset means, and what it costs |
GET /api/version |
The versions in play: lemonfiber, the stack it operates, and the container engine |
GET /api/explain |
Every word this product explains, or what the one named in ?word= means |
GET /api/front-door |
The one address to hand somebody who lives here, and why nothing else listed is it |
GET /api/provenance |
Where each service comes from: its licence, its project, and the version this stack pins |
GET /api/catalogue |
What each service is for, what its loss costs, and what became of any the stack dropped |
GET /api/outbound |
Everything that leaves this machine, what each carries, and what switching it off stops |
GET /api/stored |
What lemonfiber keeps on this machine, where each thing is, and why |
GET /api/backups |
Which backups are here to restore from, by name |
GET /api/clients |
Which app to watch on, for each kind of device somebody in the house has |
GET /api/credentials |
Every credential the stack holds, what each is for and where it stands — never a value |
GET /api/alerts |
What the operator is told about, and what changing that comes to |
GET /api/space |
Where the disk went, what is on it, and what could be got back |
GET /api/bandwidth |
What the line carries, what the stack is held to, and whether it is keeping to it |
GET /api/hosting |
What this machine keeps running when no terminal is open |
GET /api/uninstall |
What a removal would take; the removal being read is named rather than defaulted |
GET /api/migration |
What is already on this machine, before anything is proposed |
GET /api/history |
What has already been changed on this machine, and when |
GET /api/update |
?what=self or ?what=stack is required: where this copy stands and how it was installed, or what each service would move to; ?to= names a version |
GET /api/news |
What is new: releases, the household’s requests and the problems the checks found, each newest first |
GET /api/plugins |
Which plugins are installed, as lemonfiber plugin installed answers |
GET /api/wiring |
What the stack wires to what, and how each link was settled |
GET /api/settings |
Every setting the stack’s services read, as lemonfiber settings --json answers; only the operator is answered, and a secret’s value never |
GET /api/playing |
What the media server is playing now, each session naming the member watching and what they are watching; a member is answered with their own sessions only |
GET /api/watching |
What one member was part-way through and how far, most recent first. ?member= is required; ?most= defaults to 24 and is refused, not trimmed, above 500 |
GET /api/askable |
A member’s search for titles the house does not hold yet, under their own limits; ?term= is required, ?after= turns the page and ?member= names whose |
GET /api/bundle/{name} |
The support bundle itself, handed over rather than described |
Two of them are not shaped like the rest. GET /api/front-door takes no parameters, because the question takes none: which one address the household is given is worked out from what the stack runs rather than asked for, and a parameter here would be a way for one surface to be told a different door from another. GET /api/bundle/{name} is the one read that does not answer with an envelope — it answers with the bundle itself, because a browser has no path on the host to be told and handing the file over is the only form --out can take on a screen. The name is resolved beneath the bundles directory rather than followed, so one carrying a path, or climbing out of that directory, is refused by name.
Query parameters are what the commands themselves take, and only what reads. A read looks and does not touch: narrowing a diagnosis is a parameter here, while accepting a warning or running the checks that disturb a running stack changes something and belongs where changes are asked for.
POST /api/actions/<name> is where changes are asked for. An action that only
reads and writes lemonfiber’s own files is answered with its outcome, because it
has already finished by the time it could be answered. One that reaches the
container engine runs for minutes, so it is answered 202 with a job envelope
naming the work, and the work runs somewhere the connection cannot reach — a
browser tab closed mid-repair takes nothing with it. GET /api/jobs/{job} asks
what became of that work: 202 with the same job envelope while it is still
going, and the envelope it came to once it has finished. DELETE /api/jobs/{job}
ends it and answers with where it then stood. A name this run never handed out
is 404 with ASK-7, rather than reported as unfinished.
Setup has its own small surface, because it is a conversation rather than a
command: GET /api/setup says where a run stands, and POST /api/setup/answer,
/next, /back, /apply and /recover move it.
GET /api/events is a server-sent event stream, and it is described in full under
what a client keeps below. Each
event’s name is the envelope’s kind: dashboard for the state the stack is in,
plugins and wiring for what is installed and what the stack wires to what,
each said when a client joins and again when it changes, alert when an alert
starts and again when it resolves, and step, start and log for the lines a
walkthrough, a start or a followed log writes as they are written. A line said by work an action handed to a job names that job in the
envelope’s job. A member’s stream carries none of these: it carries their own
household row, their held shelf and what they are playing, each the answer
that read gives them.
When a read is refused
Section titled “When a read is refused”However a read is refused, it answers with the error envelope — the same document
--json writes — and its code says which refusal it was. The READ codes
are a read’s own. The status says which of four sorts of refusal it is:
| Refused because | Status |
|---|---|
| What the request named is not one of the things there are | 404 |
| The request could not be answered as it was asked | 400 |
| Other work holds what it needs; the same request is answered after it | 409 |
| Nothing about the request was wrong; this machine could not answer it | 500 |
The line between the first two is what the request was for. A word this product
does not explain is absent — the word is the whole of what /api/explain was asked
for, and there is no entry — while a parameter left out, given twice, or given a
value the surface does not offer is a request that could never have been answered
as it stands.
500 is reserved rather than incidental. A client told the machine failed will try
again, and a client told that about a word with no entry will try forever. Which of
the four applies is decided where the refusal is raised, and it reaches a client as
the status alone: the envelope carries no field for it.
What a request has to carry
Section titled “What a request has to carry”Every request meets the same guard, applied over the whole route tree rather than written into each handler — so an endpoint added later is guarded by having been added.
| Header | Rule |
|---|---|
X-Lemonfiber-Token |
The run’s token, on every request. The stream too: a connection held open is a request that has not finished, not one never made |
Host |
Must name the address the server is listening on. A request without one is refused — a Host check is what still holds when DNS rebinding has defeated an origin check |
Origin |
Where a browser sent one, it must name the same address. Its absence is allowed, because it is a browser’s word about itself and a client that is not a browser has none to give |
Nothing under /api/ is answered in prose. Every refusal is the error envelope,
and its code is what a client decides on, because one status covers refusals
with different remedies. Every refusal the guard makes is 403: ADMIT-4 where
the request carried no token or session this run admits, and ADMIT-5 where it
said it came from somewhere this server is not. A browser offered 401 would ask
for credentials it has no way to supply. The one thing the guard does not hide is
which paths exist: a path under /api/ that no route declares is answered 404
with ASK-9 by the app’s fallback, while one that exists answers 403, so an
unadmitted caller can tell the two apart. The endpoints are named in the published
contract, so what that reveals is already public. A path that exists, asked with a
method it does not answer, is 405 with ASK-10.
Signing in with a password
Section titled “Signing in with a password”The token printed at start answers one question — is this the machine’s own
operator — and is no use to somebody holding a phone. POST /api/session is the
one route that answers a request carrying no token: it takes
{"password": "…"}, and optionally a name, and exchanges them once for a
session, which travels in the same X-Lemonfiber-Token header. The Host and
Origin rules still apply to it. A wrong password answers 401 with ADMIT-8,
the one status that says trying again could help. Too many of them answer 429
with ADMIT-9, and a Retry-After header saying how long to wait.
The machine’s own password is tried first and admits the operator. Otherwise the
name and password are checked against the media server’s household, and a match
admits that member; the admission envelope then carries their id, and its
absence means the operator. A member’s session is checked with the media server
again on every request.
A member may ask for three things, each narrowed to themselves whatever the
request named: what the household asked for, GET /api/held for their own shelf,
and GET /api/playing for their own sessions. Everything else — every other read and every action — is refused 403
with ADMIT-6: This is not something this account may ask for. Where the media
server cannot be asked, the answer is 403 with ADMIT-7, saying nobody was
identified and nothing about the account changed, so the thing to fix is the media
server rather than the account.
The app served beside it
Section titled “The app served beside it”The binary carries a web app, embedded exactly as the stack beside it is: a
submodule at assets/web, pinned to an exact revision and never to a branch.
What is pinned there is the app’s built tree rather than its source — cutting
a version tag on lemonfiber-web builds it and pushes the output to a tag of its
own — so what a binary carries is addressable as a revision and can be checked by
its contents rather than fetched while the binary is being built.
The pair is held together at compile time. The app declares the wire version it speaks, the build compares it against the one this binary serves, and a build whose two halves disagree stops and names both numbers. An incompatible pairing therefore cannot ship, because it cannot be built — the same argument the embedded stack rests on, for the other embedded thing.
An absent app is not a mismatch. A build told to carry none says so rather than
answering with an empty document, the endpoints below /api answer as usual, and
lemonfiber ui --assets <PATH> serves a directory instead, which is what somebody
working on the app itself needs.
What a stack says it can do
Section titled “What a stack says it can do”GET /api/capabilities answers with what this stack can do, as the credential
that asked may do it, so a client on a phone reads it rather than deducing it
from a version. The payload is kind: "capabilities", and its data maps each
request, named by the path it is served at, to one of three states:
| State | Means |
|---|---|
available |
This stack can do it, and this credential may |
unconfigured |
The stack has it, and a setting switched off has to be on |
unpermitted |
The stack has it, and this credential may not ask for it |
A request the stack does not have is absent from the map rather than listed, so a stack older than a client’s request says so by not naming it. The set is generated from the lists the surface routes by, and each state is decided by the same rule every request meets.
What a client keeps that the schema cannot say
Section titled “What a client keeps that the schema cannot say”The generated artefact fixes what the wire looks like. These are the rules about what it means, and every client implements and tests them.
| Rule | Why it is a rule rather than a suggestion |
|---|---|
The per-run token travels in the X-Lemonfiber-Token header, never in a URL |
URLs reach logs, history and referrers; a credential that lands in any of those has leaked |
| The base address must resolve to loopback, and a loopback address is not refused for being named rather than numeric | Refusing the word localhost outright is the wrong trade — it is what an operator types and what a printed address may contain. Refusing a name that resolves off loopback is the protection that matters |
| There is no default port | The binary chooses a free one unless told otherwise and prints the whole address; a client is configured with that address rather than assembling one |
| The stream emits a heartbeat at least every 15 seconds, and a client treats twice that in silence as broken | Without it, “nothing has changed” and “the connection died twenty minutes ago” look identical |
Every event carries an id; a resuming client sends the last one it saw as Last-Event-ID |
Resumption decides what is retransmitted, not what is current |
| Anything held from before a gap is stale until replaced | A reconnected stream that presents pre-gap values as current is lying about what it missed |
An api_version a client cannot speak is refused plainly, naming both versions |
Rendering a partial view of fields whose meaning has quietly changed is worse than stopping |
Where the shapes come from
Section titled “Where the shapes come from”The artefact is contract/web-api/ in the lemonfiber repository, one file per
kind and per shared definition, built from the Rust types that actually serialise the reply. It is never
hand-written, and regenerating it must produce no diff — a serialised shape that
changes without the artefact changing with it fails the build rather than
reaching a client.
It is published with every release. An SDK does not fetch it while it builds; it vendors a copy pinned to an exact revision recorded beside it, so a contract change arrives as a diff somebody reads rather than as something that happens to a build nobody was watching. Generating from a version an SDK does not implement is refused, naming both versions, and writes nothing: types that compile and lie are worse than a build that stops.
Where to go next
Section titled “Where to go next”Every payload kind lists what data holds for each kind. The
two numbers in play — the package version and api_version — do different jobs,
which is two version numbers. To consume it from
code, start with the TypeScript SDK,
the PHP SDK or the Python SDK.