Skip to main content

HTTP API Reference

Endpoint-by-endpoint reference for lora-server — the Axum-based HTTP wrapper around the engine. Reach for this page when you're calling LoraDB over the wire from a stack without an in-process binding, or when you want to poke at the engine from curl. One process serves exactly one graph, optionally paired with snapshots and WAL-backed recovery.

For an install-and-run walkthrough (how to start the server, set host and port, embed it in a larger Axum app), see the HTTP server quickstart.

Endpoints at a glance

MethodPathPurpose
GET/healthLiveness probe
POST/queryRun a Cypher query
POST/explainCompile a query and return its plan — never executes
POST/profileExecute a query and return runtime metrics
POST/admin/snapshot/saveSave a snapshot (opt-in; only when --snapshot-path is set)
POST/admin/snapshot/loadRestore a snapshot (opt-in; only when --snapshot-path is set)
POST/admin/checkpointWrite a checkpoint snapshot (opt-in; only when --wal-dir is set)
POST/admin/wal/statusInspect WAL state (opt-in; only when --wal-dir is set)
POST/admin/wal/truncateTruncate safe WAL history (opt-in; only when --wal-dir is set)

Anything else returns 404.

GET /health

Returns 200 OK if the process is alive.

Request

GET /health HTTP/1.1

Response

{ "status": "ok" }

POST /query

Run a single Cypher statement (or multi-statement document) and get a structured result back.

Request body

{
"query": "MATCH (p:Person) RETURN p",
"format": "rows"
}
FieldTypeRequiredDescription
querystringyesCypher source.
formatstringnoOne of "rows", "rowArrays", "graph", "combined". Defaults to "graph".
caution

The request body does not yet accept a params field. Bind parameters via the Rust, Node, Python, or WASM bindings. See Limitations → Parameters.

content-type: application/json is required. Anything else yields 400.

Response (success)

200 OK, body is a JSON object whose shape depends on format. See Result formats for each shape in detail.

Quick reference:

formatBody shape
rows{ "rows": [ { col: value, ... } ] }
rowArrays{ "columns": [...], "rows": [[...], ...] }
graph{ "graph": { "nodes": [...], "relationships": [...] } }
combined{ "columns": [...], "data": [...], "graph": {...} }

Response (error)

Non-2xx responses use the same structured error body:

{
"error": {
"code": "LORA_PARSE",
"message": "parse error: expected ')' at position 17",
"category": "client"
}
}

code is the stable field to branch on; message is human-facing and may be reworded. category is "client" for caller errors and "server" for engine or durability failures. Parse/semantic/read-only/config errors return 400, timeouts return 408, not-found errors return 404, invalid params/vectors return 422, constraint violations return 409, WAL-poisoned errors return 503, and server/storage failures return 500.

POST /explain

Compile a query and return its plan as JSON. The executor is never invoked. Mutating queries (CREATE, MERGE, SET, DELETE, REMOVE) leave the graph untouched — /explain is a pure planning call.

Request body

{
"query": "MATCH (p:Person) WHERE p.name = $name RETURN p",
"params": { "name": "Alice" }
}
FieldTypeRequiredDescription
querystringyesCypher source.
paramsobjectnoBound parameters keyed by name. Accepted for API symmetry with /profile and /query; reserved for a future cost model.

Response

{
"query": "MATCH (p:Person) WHERE p.name = $name RETURN p",
"shape": "readOnly",
"resultColumns": ["p"],
"tree": {
"id": 3,
"operator": "Projection",
"details": { "distinct": "false", "include_existing": "false", "items": "p" },
"estimatedRows": null,
"children": [
{
"id": 2,
"operator": "Filter",
"details": { "predicate": "..." },
"estimatedRows": null,
"children": [
{
"id": 1,
"operator": "NodeByLabelScan",
"details": { "var": "v0", "labels": "Person" },
"estimatedRows": null,
"children": []
}
]
}
]
}
}

shape is "readOnly" or "mutating". details values are opaque human-readable strings; do not parse them programmatically. estimatedRows is reserved for a future cost model and is null today.

POST /profile

Execute a query and return the plan plus runtime metrics.

PROFILE executes the query for real

Mutating queries (CREATE, MERGE, SET, DELETE, REMOVE) produce the same side effects as /query: the WAL is written, snapshots observe the commit, and the live store advances.

Use /explain to inspect a mutating plan without running it.

Request body

Identical to /explain.

Response

{
"plan": { "...same shape as /explain..." },
"metrics": {
"totalElapsedNs": 124500,
"totalRows": 3,
"mutated": false,
"perOperator": {
"1": { "rows": 5, "dbHits": 0, "elapsedNs": 18200, "nextCalls": 6 },
"2": { "rows": 4, "dbHits": 0, "elapsedNs": 21100, "nextCalls": 5 },
"3": { "rows": 4, "dbHits": 0, "elapsedNs": 24400, "nextCalls": 5 }
}
}
}

perOperator keys are physical-node ids matching tree[*].id from the plan. Per-operator elapsedNs is wall-clock time inclusive of descendants — that's the "operator + everything below it" view that matches what is visually surprising when reading a profile. dbHits is reserved for a future phase and is 0 today.

Admin endpoints (opt-in)

The admin surface is split in two:

  • POST /admin/snapshot/save and POST /admin/snapshot/load mount only when lora-server starts with --snapshot-path <PATH>.
  • POST /admin/checkpoint, POST /admin/wal/status, and POST /admin/wal/truncate mount only when lora-server starts with --wal-dir <DIR>.

The two flags are independent. You can run snapshot admin without WAL, WAL admin without snapshot save/load, or both together. See the HTTP server quickstart, the canonical Snapshots guide, and WAL and checkpoints.

Security

The admin endpoints have no authentication, and the optional path body field is passed straight to the OS — any client that can reach the admin port can write files anywhere the server UID can write, or swap the live graph by pointing load at an attacker-staged file. The same warning applies to /admin/checkpoint. Do not expose them on an untrusted network. See Limitations → HTTP server.

Snapshot save / load request body

Both endpoints accept the same optional JSON body:

{ "path": "/custom/location/snapshot.bin" }
FieldTypeRequiredDescription
pathstringnoOverride the server's default --snapshot-path for this request only. Omit the body (or omit path) to use the configured default.

content-type: application/json is required when sending a body.

Snapshot save / load response (success)

200 OK, body is a SnapshotMeta:

{
"formatVersion": 1,
"nodeCount": 1024,
"relationshipCount": 4096,
"walLsn": null,
"path": "/var/lib/lora/db.bin"
}
FieldTypeDescription
formatVersionnumberThe snapshot file format version (currently 1).
nodeCountnumberNodes in the saved / loaded graph.
relationshipCountnumberRelationships in the saved / loaded graph.
walLsnnumber or nullnull for a pure snapshot; non-null for a checkpoint snapshot written with WAL enabled.
pathstringFilesystem path the server actually used.

Snapshot save / load response (error)

  • 500 Internal Server Error — path cannot be read / written, file is corrupt, permissions fail, parent directory is missing, or the save / load itself errors.
  • 404 Not Found — the server was not started with --snapshot-path, so the admin routes are not mounted.

Error bodies match /query's shape:

{
"error": {
"code": "LORA_SNAPSHOT_CODEC",
"message": "snapshot load failed: bad magic",
"category": "server"
}
}

Examples

# Save to the configured default path
curl -sX POST http://127.0.0.1:4747/admin/snapshot/save

# Save to an override path
curl -sX POST http://127.0.0.1:4747/admin/snapshot/save \
-H 'content-type: application/json' \
-d '{"path": "/var/backups/lora/2026-04-24.bin"}'

# Load from the configured default path
curl -sX POST http://127.0.0.1:4747/admin/snapshot/load

POST /admin/checkpoint (opt-in)

Mounted only when the server starts with --wal-dir <DIR>. The route does not require /admin/snapshot/save or /admin/snapshot/load to be mounted.

Request body

Optional JSON body with a conditional field:

{ "path": "/var/lib/lora/checkpoint.bin" }
FieldTypeRequiredDescription
pathstringconditionalTarget snapshot path for the checkpoint. Required unless the server was started with --snapshot-path.

If --snapshot-path is configured, omitting the body uses that path as the default checkpoint target. Without --snapshot-path, the body must include path or the route returns 400 Bad Request.

Response (success)

200 OK, body matches the snapshot response shape:

{
"formatVersion": 1,
"nodeCount": 1024,
"relationshipCount": 4096,
"walLsn": 4815,
"path": "/var/lib/lora/checkpoint.bin"
}

The important difference is walLsn: a checkpoint stamps the snapshot with the WAL's durable fence. Call sync() or use an explicit checkpoint when you need an immediate GroupSync durability boundary.

Examples

# WAL-only server: pass a checkpoint path explicitly.
curl -sX POST http://127.0.0.1:4747/admin/checkpoint \
-H 'content-type: application/json' \
-d '{"path": "/var/lib/lora/checkpoint.bin"}'

# Server started with --snapshot-path: the body can be omitted.
curl -sX POST http://127.0.0.1:4747/admin/checkpoint

Response (error)

  • 400 Bad Request — no path in the request body and no --snapshot-path configured on the server.
  • 404 Not Found — the server was not started with --wal-dir.
  • 500 Internal Server Error — the checkpoint write itself failed.

POST /admin/wal/status (opt-in)

Mounted only when the server starts with --wal-dir <DIR>.

Request

No body.

Response (success)

200 OK:

{
"durableLsn": 4815,
"nextLsn": 4820,
"activeSegmentId": 3,
"oldestSegmentId": 2,
"bgFailure": null
}
FieldTypeDescription
durableLsnnumberHighest LSN known durable on disk. In none sync mode, this is only a logical checkpoint fence.
nextLsnnumberNext LSN the WAL will allocate.
activeSegmentIdnumberNumeric id of the segment currently accepting appends.
oldestSegmentIdnumberNumeric id of the oldest retained segment.
bgFailurestring or nullLatched background fsync failure, populated when GroupSync goes unhealthy.

Response (error)

  • 404 Not Found — the server was not started with --wal-dir.
  • 500 Internal Server Error — WAL status could not be read.

POST /admin/wal/truncate (opt-in)

Mounted only when the server starts with --wal-dir <DIR>.

Request body

Optional JSON body:

{ "fenceLsn": 4815 }

If omitted, the server truncates up to the WAL's current durableLsn. Only sealed segments are removed; the active segment and the segment immediately before it are retained.

Response (success)

204 No Content

Response (error)

  • 404 Not Found — the server was not started with --wal-dir.
  • 500 Internal Server Error — truncation failed or WAL status could not be read for the default fence.

Examples

Minimal round-trip

curl -s http://127.0.0.1:4747/query \
-H 'content-type: application/json' \
-d '{"query": "CREATE (:Person {name: \"Ada\"})"}'

curl -s http://127.0.0.1:4747/query \
-H 'content-type: application/json' \
-d '{"query": "MATCH (p:Person) RETURN p.name AS name", "format": "rows"}'

The first call writes a node; its body is {"graph": {"nodes": [...], "relationships": []}} because the engine default is graph and CREATE contributes the new node. The second call asks for rows explicitly and returns {"rows": [{"name": "Ada"}]} — one row per match, keyed by the AS alias.

Choose a result format

# Column-indexed (smallest payload for wide result sets)
curl -s http://127.0.0.1:4747/query \
-H 'content-type: application/json' \
-d '{"query": "MATCH (p:Person) RETURN p.name, p.born",
"format": "rowArrays"}'

# Graph (nodes + edges, de-duplicated)
curl -s http://127.0.0.1:4747/query \
-H 'content-type: application/json' \
-d '{"query": "MATCH (a)-[r]->(b) RETURN a, r, b",
"format": "graph"}'

rowArrays comes back as {"columns": [...], "rows": [[...], ...]} — one columns list plus one tuple per row, so the column keys aren't repeated. graph returns {"graph": {"nodes": [...], "relationships": [...]}} with each entity listed once even when many rows reference it — ideal for visualisers. See Result formats for the full shape of each.

Node client

async function runQuery(query: string, format = 'rows') {
const res = await fetch('http://127.0.0.1:4747/query', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ query, format }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error?.message ?? `http ${res.status}`);
}
return res.json();
}

Configuration

lora-server takes its bind address from:

  1. CLI flags: --host, --port.
  2. Env vars: LORA_SERVER_HOST, LORA_SERVER_PORT.
  3. Defaults: 127.0.0.1:4747.
lora-server --host 0.0.0.0 --port 8080
LORA_SERVER_HOST=0.0.0.0 LORA_SERVER_PORT=8080 lora-server

Full walkthrough in the HTTP server quickstart.

Relevant durability flags:

  • --snapshot-path <PATH> / LORA_SERVER_SNAPSHOT_PATH
  • --restore-from <PATH>
  • --wal-dir <DIR> / LORA_SERVER_WAL_DIR
  • --wal-sync-mode <MODE> / LORA_SERVER_WAL_SYNC_MODE

What isn't here

  • Authentication — not supported. Bind to 127.0.0.1 or put the server behind a reverse proxy.
  • TLS — not supported. Terminate at a proxy.
  • Rate limiting — not supported.
  • Parameters — not yet supported. See Limitations → Parameters.
  • Multi-database — not supported. One process, one graph. Run multiple processes on different ports for isolation.
  • HTTP auth / TLS on the admin surface — not supported. Snapshot and WAL admin routes are opt-in, but still unauthenticated when enabled.

See also