Security and isolation

ShippedWorks today.

One machine per book, no public address on any of them, three separate authorisation layers in front — and 404 rather than 403, so a credential cannot be used to learn which books exist.

One machine per book

Your ledger is not a row in a shared table. Each book runs its own machine with its own volume, and none of them has a public address. The only road in is /v1/books/<book-id>/api/* on the control plane, which resolves your access, then addresses exactly one machine over a private network.

That is structural rather than a preference. A public address per book would put every tenant’s machine on the open internet behind nothing but the ledger UI’s own basic auth, and the standing invariant is that neither the runtime nor the read-only UI is ever exposed without the authorisation layer in front.

Three layers, kept distinct

  1. Org membershipowner or staff. Governs the workspace and its billing boundary. The org owner implicitly holds owner rights on everything in it.
  2. Access grantsowner, accountant or client, on a project (all its books) or on a single book. This is what decides the ledger scope you get: owner and accountant write, client reads.
  3. API-key scopesread or readwrite, per key, per book. A key issued for one book can never reach another.

They are layered, not interchangeable, and the narrowest wins: a requested scope is clamped to the ceiling your role allows, and a read key can only ever mint a read token.

404, not 403

A caller with no access to a book gets 404. Not 403 — a 403 confirms the book exists, which is exactly what an unauthorised caller must not learn.

A caller who can see the book but attempts something their role does not permit gets 403 forbidden. Only principals who already have access ever reach that branch, so it leaks nothing.

What the book itself checks

The machine holds no user table and knows nothing about orgs. It verifies four things on every request and refuses on any of them: the token’s HS256 signature, that its book_id equals the book id injected into that machine, that it has not expired (60 seconds of clock leeway), and that its scope covers the operation. A write with a read token is 403.

Tokens are short and, through the proxy, invisible

A book token minted for you directly lives 15 minutes by default and 60 minutes at the absolute ceiling.

Through the proxy it is shorter and you never see it: the proxy mints a 60-second token per request and never returns it. That is what makes revocation mean what the page says — the key is checked against the database on every single request, so the first call after you revoke a key fails. A client holding its own minted JWT would keep write access until that token expired, on a laptop, with no way to recall it.

The browser ledger

The full ledger UI runs read-only, and the proxy in front of it accepts GET and HEAD only. Writes have exactly one path — the API — so every change to your books has an author and a commit.

What there is not

No two-factor authentication and no SSO. Sign-in is Google or email and password. If your threat model needs more than that today, it is missing.

No public per-book address, and no plan to add one. If you were hoping to point your own tooling straight at the machine, you point it at the proxy instead.

No webhooks and no public OpenAPI document. Nothing pushes to you, and the surface is documented here rather than machine-readable.

No separate audit log. Who changed the ledger is in the git history; who changed access is visible on the collaborators page, and that is the whole of it.

Something here wrong or missing? Put it on the board — it is public, and the reply is in the thread.