The RankeDB Operator's Manual
Configuring and running an instance: the launch artifact field by field, and the access policy it carries
v1.19.031 August 2026
Imported from ranke-db v1.19.0 on 9 September 2026 — see the repository for anything newer.
RankeDB serves one Ranke-Archive from one configuration file. The file is the whole of the deployment: it names where claims are kept, what advances the head, the identity merges are signed under, where secrets come from, which addresses are served, and who may act on what. Nothing else configures an instance, and nothing reconfigures one while it runs.
This manual is that file, field by field. It says what each section selects, which backends a section may name and what fields each of those takes, and how the account roster decides what a request is allowed to do. What a claim is, how a closure is verified, and the query language a read is written in belong to the Ranke-Graph specification and the ranke-go library that implements it; the REST surface itself is fixed by the OpenAPI contract, openapi/openapi.gen.yaml, which ships in this manual’s own download.
1 Running an instance
A ranke-db instance is one binary and one configuration file. Put the binary somewhere, write the file, and launch: the configuration says where the data lives, which addresses are served, and who may act on the archive.
ranke-db run config.jsonThat serves until you stop it (Ctrl-C, or a SIGTERM). A configuration is read once, at launch: an instance is never reconfigured while it runs, so the administrative cycle is edit, run, observe, stop, edit again. Two instances that differ in any way are two processes with two files.
1.1 Checking a configuration before launching
ranke-db verify config.json answers config ok or the first problem it finds. It checks to one of three depths, and each depth includes the ones before it.
--level syntaxadmit list naming an account that does not exist, and a malformed endpoint address all fail here. It touches neither the environment nor the network, so it is the check to run in a pipeline that holds no secrets.--level resolveenv(...) and vault(...) reference, so a missing variable or an unreadable secret fails before launch. This dials the vault if — and only if — some value references it.--level connect1.2 The shape of the document
The configuration is one JSON object with six top-level sections, and no others — an unrecognised section is refused rather than ignored, so a misspelling never passes silently.
storagesequencersignervaultvault(...) references read from. Optional; needed only if some value uses one. See Section 2.4.accountsendpointsThe first four are the ports the server drives; the last two drive it. Each section names a backend under "type" and carries that backend’s own fields beside it.
A section left out yields no adapter, which is why verify --level syntax passes on a partial document: a file worth launching names at least storage, sequencer, signer and one endpoint.
1.3 Values, secrets, and where they come from
Any string value in any section may be written as a delegation instead of a literal.
env(NAME)vault(REF)vault section configures."key": "env(RANKE_SIGNER_KEY)",
"token": "vault(kv/rankedb/openbao-token)"The match is whole-value: a delegation is the entire string or it is not one at all, so no secret hides inside a longer value, and "addr": "env(PORT):8080" is a literal address rather than a substitution. A reference that resolves to nothing is an error — resolution fails loudly rather than yield an empty secret.
Resolution happens at use. The environment is always available; the vault is built from its own section the first time some other value asks for a secret, which is why that section may use env(...) and never vault(...). Nothing resolves during a syntax check.
1.4 Encrypting the whole file
A configuration may be stored encrypted with age, passphrase-based, and both run and verify accept it as it stands: the binary detects the encryption and decrypts before parsing.
--age-key prompt|stdin|env:VAR|file:pathprompt asks on the terminal.ranke-db run --age-key prompt config.json.age
ranke-db run --age-key env:AGE_KEY config.json.age
ranke-db run --age-key file:/run/key config.json.ageReading both the configuration and the passphrase from standard input is refused, since the two cannot be told apart.
Encryption covers the file as a whole, so it protects the values written literally in it. A deployment that keeps its secrets in the environment or a vault gets the same protection without the passphrase, and the two combine.
1.5 The development clock
--devPOST /dev/clock, which steers the time the Sequencer stamps merges with instead of following real time. It requires sequencer.type to be "dev", and exists to make a test suite’s timeline reproducible.2 The ports the server drives
Four sections name backends the server reaches out to: storage, sequencer, signer and vault. Each is an object naming its backend under "type" and carrying that backend’s fields beside it. Everything below is written from the configuration’s side — the field to write, what it selects, and what happens when it is absent.
2.1 storage — where claims live
The storage section describes the Universe as a tree: composite types hold other descriptors, leaf types name a technology. The whole section is one descriptor, so the simplest storage is a single leaf, and the usual shape is a stack of them.
2.1.1 Composites
stacklayers, an array of descriptors read in order, first entry first. A read tries each layer in turn and a miss is repaired from the layer below, which is what makes a cache in front of a slow backing store useful. An empty array is refused.partitionshards, an array of descriptors. Content is routed to one shard by its id, so the shards divide the data rather than duplicate it. An empty array is refused.Either composite may hold the other, and either may hold a leaf, to whatever depth the deployment wants.
2.1.2 Leaves
memminimalmem instead.fsdir, a directory path. The store is files under it, created as needed.sqlitedsn, a SQLite data-source name — a file path, or :memory:.s3bucket and region, plus accessKeyId and secretAccessKey, all four required. Credentials come from this configuration alone (as literals or through env(...)/vault(...)) and never from an ambient credential chain, so one file accounts for a deployment’s secrets. endpoint and usePathStyle ("true") point the same backend at an S3-compatible service such as MinIO.redisaddr (host:port), with optional password and db, the numeric database index.neo4juri, required. username and password go together — give neither and the driver connects without authentication, give one and the other becomes required. database selects a database other than the default.2.1.3 Fields every descriptor accepts
namemodeauthoritative, eager, background or lazy. An assertion rather than a setting: a backend serves the tier it serves, and a mode that disagrees fails the launch.maxContentSizeWrite the two assertions where a deployment wants a mismatch caught, and leave them out otherwise.
"storage": {
"type": "stack",
"layers": [
{"type": "mem", "name": "cache"},
{"type": "fs", "name": "archive", "dir": "/var/lib/rankedb/blobs"}
]
}2.2 sequencer — how the head advances
The sequencer moves the branch table head as contributions merge, and keeps the previous heads so a deployment can look back at where the archive stood.
type"concurrent" or "dev". concurrent is the one to deploy: it prepares contributions in parallel and folds a whole batch into one advance of the head. dev is the blocking reference sequencer — one contribution at a time, callers queueing behind it — for tests and development, and it is what --dev requires.history{"type": "mem"} keeps it in memory, and is the default when the field is absent; {"type": "file",
"path": "..."} keeps it in a file.The history choice decides whether a restart reopens the existing archive or bootstraps an empty one, so a deployment that keeps its storage on disk wants its history there too.
"sequencer": {
"type": "concurrent",
"history": {"type": "file", "path": "/var/lib/rankedb/head.log"}
}2.3 signer — the identity merges are attested with
This is the server’s own key, and it signs merges. It never signs a contributor’s claims: those arrive signed already, by the key of whoever made them.
inmemorykey, an Ed25519 private key as PKCS#8 PEM, required — this backend mints nothing and needs to be given one. Writing it as env(...)/vault(...) keeps the key out of the file.openbaoaddress (server URL) and token are required; mount is the Transit mount path, "transit" by default; key names the Transit key.2.4 vault — where vault(...) reads from
Configure this section only if some value in the document is written as vault(REF). It is built the first time such a value is read, and its own fields may use env(...) — a vault cannot bootstrap itself out of the store it is the way to.
openbaoaddress and token, both required; mount, the KV v2 mount path, "secret" by default. A vault(REF) reference names the path within that mount.azureurl, an Azure Key Vault URI. The backend accepts its configuration and refuses every read — secret resolution is not wired yet, so a deployment cannot rely on it."vault": {"type": "openbao", "address": "https://bao.internal:8200",
"token": "env(BAO_TOKEN)", "mount": "secret"},
"signer": {"type": "inmemory", "key": "vault(rankedb/signing-key)"}3 Endpoints and authentication
endpoints is an array, and each entry is one address the instance serves.
"endpoints": [{
"transport": {"type": "rest", "addr": ":8080"},
"auth": [{"type": "apikey", "keys": [{"account": "ops", "sha256": "…"}]}],
"admit": ["ops"]
}]transportauthadmitAn instance may serve several endpoints at once — a public port and a socket for administration, say — and each carries its own authentication and its own admitted accounts. Nothing is shared between them but the archive underneath.
3.1 transport
type"rest" (or "rest_http") serves the REST API over HTTP. "mcp" (or "mcp_http") is reserved for the MCP transport, which the server refuses at launch until it is implemented.addr":8080" and "127.0.0.1:8080" are TCP; "unix:///run/rankedb/admin.sock" is a Unix domain socket. The scheme is what decides — a bare path is refused with the correction, and an overlong socket path is refused too, both by verify --level syntax rather than at launch.mode"0600" — the user the server runs as, and nobody else.groupallowedOriginsenv(...) can supply it per deployment.A socket suits an endpoint that should not be reachable over the network at all. Forwarding one to a workstation is enough to use it:
ssh -L 8080:/run/rankedb/admin.sock hostafter which http://127.0.0.1:8080 reaches the endpoint exactly as a network address would.
3.2 auth
Each entry configures one authenticator, and each authenticator consumes one credential scheme. A request is routed by the scheme it presents — never guessed from the token’s bytes — so two entries claiming one scheme are refused at launch. A request presenting two credentials at once is answered 400, and one presenting none is answered 401 unless the endpoint configures noauth.
3.2.1 noauth
Authenticates every request as one fixed account. It consumes no credential and so acts as the endpoint’s fallback.
subject"anonymous".3.2.2 apikey
Presented as the X-API-Key header. Keys shorter than 16 characters are rejected.
keys{"account": …, "sha256": …}. The configuration holds each key’s SHA-256 digest as lowercase hex, never the key itself, so a leaked file yields no usable credential — and the server cannot tell you a key it has forgotten.3.2.3 jwt
Presented as Authorization: Bearer ….
algorithmHS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512, EdDSA. Taken from the configuration and never from the token.keyHS* algorithm. Exclusive with jwks_url, and one of the two is required.jwks_urljwks_refresh5m.account_claimsub.audienceissuer3.2.4 macaroon
Presented as Authorization: Macaroon …. The token’s identifier is the account and its first-party caveats are read as grants, which narrow what that account may do for this request alone (Section 4.4).
root_key3.3 admit
admit lists the accounts this endpoint can act as. An account absent from every admit list is defined and unreachable, and an admit entry naming an account the roster does not define fails verify --level syntax.
Admission is per endpoint, and it is what separates one endpoint from another: an authenticator may resolve a request to any account it likes, but a request lands on the endpoint’s own roster, and an account it does not admit gets nothing.
4 Accounts and access
Who may read and who may contribute is a property of this server, not of the archive it serves. It belongs in the configuration, and it is never written into the graph as a claim: copy an archive to a second deployment and none of the policy below travels with it, which is the point. The graph holds what happened; the file holds who may act here.
4.1 The roster
accounts maps an account name to the grants it holds.
"accounts": {
"reader": {"grants": ["R *", "R $branches", "R $archive"]},
"contributor": {"grants": ["CR proj-*", "R $branches"]},
"ops": {"grants": ["CR *", "CR $branches", "R $archive", "R $universe"]}
}An account is a name and a list of rights, and it carries no credential of its own. What proves a request is that account belongs to an endpoint’s auth section (Section 3.2), and which endpoints can reach it at all is admit (Section 3.3).
4.2 Grants
A grant is one string, "RIGHTS TARGET": the rights as letters run together, then what they apply to. The rights are CRUD.
RUD"CR proj-alpha" is one grant conferring two rights. Rights are additive across an account’s grants: a request is allowed when any one grant carries the right and matches the target.
The target is either a branch pattern — lowercase letters, digits and -, with * and ? as wildcards — or one of four reserved names.
$branchesR lists the branches; C is what creates one, since the table is a claim and adding a branch contributes to it.$archive$universeR applies here; any other letter is refused.$sequencerGrants are checked when the file is parsed, so an unknown right letter, a branch pattern with an illegal character, or "C $universe" fails verify --level syntax rather than at the first request.
4.3 What each operation requires
| The caller asks for | The grant it needs |
|---|---|
| Query claims | R on the queried scope |
| One claim, or its content, by id | R on the scope it is read through |
| A branch’s head, or its head and height | R on that branch |
| The branch list | R $branches |
| The archive’s head and shape | R $archive |
| Contribute claims to a branch | C on that branch |
| Create a branch | C $branches |
| Purge claims | D on every branch holding them |
| Health, storage layers, verification runs | none |
U and D are accepted in a grant, and no route exercises them yet: nothing served overlays or purges a claim, so an account holding them can do what a CR account can and nothing more.
4.4 Attenuation
A macaroon carries first-party caveats, and each is read as a grant of the opposite polarity: the account’s own grants say what it may ever do, the caveats narrow what this particular token may do, and a request must satisfy both. Caveats accumulate — every one of them must allow the request, so passing a token on after adding a caveat can only ever hand over less.
Nothing else moves rights at run time. There is no call that edits the roster, and no claim in the graph that confers one: to change who may do what, stop the instance, edit the file, and start it again.
API The REST contract
Every endpoint the server serves, and what each one is for. The contract is generated in the part's own repository and ships with its manual.
Download the contract — OpenAPI 3.0.3, ranke-db API 0.2.0, and the file to point a client generator at.
read
| Method | Path | What it does |
|---|---|---|
GET | /archive/claims/{id} | Fetch a claim within the archive head's closure |
GET | /archive/claims/{id}/content | Fetch the content of a claim within the archive head's closure |
GET | /archive/info | What is known about the archive |
GET | /branches | List the branch table's branches |
GET | /branches/{branch}/claims/{id} | Fetch a claim within a branch's closure |
GET | /branches/{branch}/claims/{id}/content | Fetch the content of a claim within a branch's closure |
GET | /branches/{branch}/head | Current head id of a branch |
GET | /branches/{branch}/info | What is known about a branch |
POST | /query | Read the graph with a declarative query |
GET | /universe/claims/{id} | Fetch a claim by id from the Universe (privileged) |
GET | /universe/claims/{id}/content | Fetch the content of a claim by id from the Universe (privileged) |
write
| Method | Path | What it does |
|---|---|---|
POST | /contribute | Contribute signed claims (atomic) |
dev
| Method | Path | What it does |
|---|---|---|
POST | /dev/clock | Advance the dev sequencer's clock |
system
| Method | Path | What it does |
|---|---|---|
GET | /health | Liveness and signer identity |
GET | /system/layers | List storage layers |
GET | /system/verifications | List verification runs |
POST | /system/verifications | Start a verification run |
DELETE | /system/verifications/{reportId} | Delete a verification run |
GET | /system/verifications/{reportId} | Show a verification run |
POST | /system/verifications/{reportId}/cancel | Cancel a running verification run |
