Running a receiver
For the operator. What you get: a receiver that holds adopters' series and friction records, stays inside the disk it shares with everything else on the host, and can stop admitting new accounts without disturbing the ones it already has — with the commands to do each of those, and what the refusals mean when somebody hits one.
If you read one thing here, read When the disk gets tight.
Running your own is unsupported. The receiver ships inside the artifact because one artifact carries everything, and this page exists because we run one — not as an invitation. You can stand one up and it will work; what we do not undertake is to keep your store working across versions. The shape of what a receiver holds on disk is working state and changes without notice. What is contracted is the wire: the record format, the versions the receiver accepts, and the specification it serves at
/spec. Build on that and either end can move.Moving a history between holders needs nothing extra —
plumb series pullfrom one andplumb series pushto the other, which are the same two mechanisms a hand-over already uses.
Starting it
plumb-friction-receiver --store DIR --registry FILE [--accounts FILE]
[--reserve MIB] [--soft-reserve MIB] [--pool MIB]
[--public-url URL] [--host HOST] [--port PORT]
| Flag | What it is |
|---|---|
--store |
where records and series are written. Must be durable — see below |
--registry |
the operator-issued credentials file |
--accounts |
self-provisioned accounts; defaults to accounts.json beside the store |
--reserve |
MiB to leave free for everything else on this volume — the hard limit |
--soft-reserve |
MiB below which registration closes. Defaults to 4 × --reserve |
--pool |
MiB this receiver may hold in total, across every account |
--public-url |
the address clients actually use. Required behind a proxy — see below |
Behind a reverse proxy, --public-url is not optional
A signature is bound to the receiver it was made for, so both ends must spell that address the same way. If nginx terminates TLS and proxies to the loopback, the process sees http://127.0.0.1:8081 while clients sign for https://your-receiver — and the failure is a nasty one, because claiming an account succeeds and every signed request afterwards fails. Claiming is the one exchange that carries no signature, so it is the one thing that keeps working.
--public-url https://friction.plumbspec.org
The refusal names what the receiver signs as, so a mismatch is diagnosable rather than mysterious. This was found by deploying, not by testing: a test that stands up its own receiver hands both ends the same string and passes whichever way this behaves.
Set all three deliberately on a shared host. The defaults suit a machine with room to spare and are wrong on one without: see a real host, worked through.
The store must be durable
Do not point --store at /tmp, /var/tmp or /dev/shm. The system clears those without warning, and a series is custodial — an emptied store reads exactly like one nothing was ever written to, so the loss is silent. The receiver says so at startup if it spots one of these, but it cannot see every ephemeral mount, so this one is yours to get right.
What it prints at startup
plumb: 41216 MiB free · registration open · door closes under 1024 MiB · writes refused under 256 MiB · holding at most 2048 MiB
The facts you will want during an incident: how much room there is, whether the front door is open, and all three limits. It also complains if --soft-reserve or --pool is set above the size of the whole filesystem — a soft limit the volume can never clear means registration never opens, and a pool larger than the disk is not a bound at all.
When the disk gets tight
There are two limits, and they give up different things. The order is deliberate: what is refused first is what nobody was promised yet, and what is never refused is what you already promised to keep.
| Free space | What stops | What keeps working |
|---|---|---|
under --soft-reserve |
new account registration | every existing account: push, pull, key rotation |
under --reserve (hard) |
accepting writes, as a retryable refusal | reads; nothing held is discarded |
| below that | — nothing further gives — | held history is never discarded to reclaim room |
The band between the two limits is the room existing accounts expand into, and sizing it is a real decision: too narrow and closing registration is decoration, too wide and you turn adopters away while sitting on space.
--soft-reserve defaults to 4 × --reserve, which is fine where there is room to spare and breaks where there is not. On a volume whose free space is comparable to the reserve, no multiple works: raise --reserve and the soft limit climbs above anything the disk can clear, so the door is shut permanently and looks broken rather than protective. That is when you set it outright.
--pool is the third: the most this receiver will ever hold, whatever the disk says. It is the number that makes committed against remaining meaningful, and the one worth setting first.
Refusals are retryable. A producer that is refused keeps its backlog and sends it on the next run, and no adopter's board turns red — a board is derived from their run and never depends on this service being up. You are costing them a delay, not a result.
Nothing is deleted to make room. Paying for our shortage by discarding somebody's history would spend the exact thing the store exists to provide. An account that exceeds its own byte allowance has its own history shortened — oldest first, recorded as discarded — and that never reaches another account's.
The door reopens by itself once free space is clear of the soft limit. There is a deliberate gap before it reopens so that a wobble in free space does not make it flap.
Turning registration off
You can always shut the front door, whatever the disk says:
plumb-friction-receiver registration --switch FILE close --reason "…"
plumb-friction-receiver registration --switch FILE open
plumb-friction-receiver registration --switch FILE show
The switch file is registration.json inside --store by default; --switch names it. It takes effect on a running receiver — no restart — because the moment you most need to shut the door is the moment you least want to bounce the process.
registration show tells you whether you closed it — it deliberately does not report the disk's half, which is derived and visible in /spec below.
--reason is what whoever tries will be told, in your words. Something true and specific beats something apologetic:
--reason "Not taking new accounts during the alpha. Your identity is good — claim again later."
Two things worth knowing:
- Closing registration disturbs nothing that came through it. Existing accounts push, pull and rotate keys exactly as before. Re-claiming an account that already exists also keeps working.
openclears your decision; it does not force the door open. If the receiver is low on room it stays closed on its own and reopens when there is room. The two halves are independent.
Somebody who hits a closed door has already generated an identity locally. It cost you nothing and it stays valid — they keep it and claim later. The refusal says so.
A real host, worked through
a00: a 9.7 GB root filesystem, 1.6 GB free, and no second volume. Sharing it: containerd (851 MB, and one image pull from growing), nginx, exim4, fail2ban, and two application services. The receiver's own state is 588 KB.
The defaults are wrong here in two ways. The 2048 MiB pool is larger than the free space, which is not a bound. And 4 × a sensible reserve puts the soft limit above anything the volume can clear, so registration would never open.
plumb-friction-receiver --store /var/lib/plumb-friction/records \
--registry /var/lib/plumb-friction/registry.json \
--reserve 512 --soft-reserve 768 --pool 256
| why | ||
|---|---|---|
--reserve 512 |
hard | survives a container image pull without refusing writes |
--soft-reserve 768 |
soft | door open at 1.6 GB free, with 832 MiB of margin, and closes early enough to matter |
--pool 256 |
total held | ~32 accounts at the 8 MiB allowance, and ~400× what is held today |
Plumb then cannot exceed 256 MiB whatever happens, and stops admitting new accounts long before it is near it. The store stays at /var/lib/plumb-friction/records: persistent, owned by the service user, and not somewhere the system clears.
Ending an account
Four endings, and they are not interchangeable — the command makes you say which:
plumb-friction-receiver account --accounts FILE [--store DIR] list
plumb-friction-receiver account --accounts FILE suspend --id ID
plumb-friction-receiver account --accounts FILE reinstate --id ID
plumb-friction-receiver account --accounts FILE --store DIR end --id ID --how HOW
account list shows what is there, account suspend and account reinstate are the reversible pair, and account end is the one that decides what becomes of a store.
--how |
what it does |
|---|---|
revoked |
the account stops authenticating. Nothing held is touched |
suspended |
writes stop and everything stays readable. Prefer suspend for a reversible pause |
closed |
reclaims the store. For an account that is genuinely finished with |
abandoned |
the same reclamation, for one whose holder can no longer reach it |
The account entry stays after any of them, and that is the point. It carries the id, which ending it was, when, and how much was reclaimed. A store removed with nothing left behind reads exactly like one that never existed — the flattering answer, from the one operation that knows better. The entry is a few hundred bytes and is the whole record that the account was ever here.
suspend is the reversible one and is what to reify for a suspicion. It stops writes and, because compaction is triggered by a write, it stops compaction too — a store held on suspicion would otherwise keep shortening from the front, discarding the oldest records, which is where the pattern that justified the suspicion is legible.
list with --store says what each account is actually holding, which is the number to look at before deciding anything.
Knowing whether you are alright
curl -s https://your-receiver/spec | jq .registration
{
"open": true,
"accounts": 3,
"committed_bytes": 25165824,
"free_bytes": 43212800000
}
committed_bytes is worst-case future growth of the accounts you already have — the population times the per-account allowance. It is a real bound rather than a guess only because the door can be shut: with registration closed the population cannot grow, so committed-against-free is the honest answer to are we going to be alright, and it is what tells you whether --reserve is set anywhere near right.
Issuing credentials by hand
Separate from self-provisioned accounts, and still here — it is how a friction contributor is admitted, which is not something anyone self-serves.
plumb-friction-receiver registry --registry FILE mint --label WHO [--relationship R]
plumb-friction-receiver registry --registry FILE list
plumb-friction-receiver registry --registry FILE revoke --label WHO
A minted credential is shown exactly once — the registry keeps only a fingerprint. list answers who has access, never what they hold. Issuing and revoking take effect immediately on a running receiver.
What a refusal means
| Code | Meaning | What the producer does |
|---|---|---|
| 401 | the credential or signature is not recognised | stops; re-enrolling is the fix |
| 403 | recognised, but this account does not hold that grant | stops; re-enrolling never fixes it |
| 409 | the counter has been used | takes the value in the reply and retries |
| 422 | the body could not be read | stops; the content is at fault |
| 426 | the record version is not accepted here | retries after negotiating |
| 503 | registration is closed | keeps the identity, claims later |
| 507 | no room to accept | keeps the backlog, sends next run |
See also
- Command line — the adopter-facing commands
- Getting started — install to first proven story