Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

REST API

The server exposes a small JSON-over-HTTP API on its --addr. Every route except /health and /version requires HTTP Basic auth when a password is set (see Authentication & TLS).

POST /cypher (alias POST /query)

Run a Cypher query. Request body:

{
  "query": "MATCH (n:Person) RETURN n.name AS name LIMIT 10",
  "db": "default",
  "at": null,
  "consistency": "local"
}
FieldRequiredMeaning
queryyesThe Cypher (or DDL) statement.
dbnoTarget database (default "default"). Overridden by USE.
atnoEngram id for a time-travel read (equivalent to USE … AS OF).
consistencyno"local" (default) or "linearizable". See below.

consistency controls read freshness (ignored for writes):

  • "local" (default) — serve this node’s applied state. Fast, causally consistent within a database, eventually consistent across replicas.
  • "linearizable" — reflect every write committed before the read began, via a leader read-index barrier. Leader-only: a follower returns 503 with a leader_id / leader_addr hint (like a misrouted write). Costs one intra-cluster round-trip. See High Availability.
curl -s -u neo4j:secret -X POST http://127.0.0.1:21001/cypher \
  -H 'content-type: application/json' \
  -d '{"query":"CREATE (n:Person {name:\"Ada\"}) RETURN n"}'

Statement routing

The handler dispatches a statement in this order:

  1. Full-text DDL (CREATE/DROP FULLTEXT INDEX, SHOW FULLTEXT INDEXES) — schema commands replicate through Raft; SHOW reads the local catalog and returns {"fulltextIndexes": [...]}.
  2. Database DDL (CREATE/DROP DATABASE → replicated; SHOW DATABASES → local registry read).
  3. A leading USE <db> selector (optionally USE <db> AS OF '<id>') picks the target database / time-travel point.
  4. Otherwise: writes go through Raft and apply on every node; reads serve from the local applied graph.

The response is JSON with the result rows (column order and per-row alignment are preserved). Errors come back as a JSON error with an appropriate HTTP status.

GET /health

Liveness check. No auth. Returns OK when the node’s HTTP server is up.

GET /version

Server version string. No auth.

Management endpoints

On --addr, for cluster operations (see High Availability):

EndpointPurpose
POST /mgmt/initInitialize a cluster (alternative to --bootstrap).
POST /mgmt/add-learnerAdd a node as a non-voting learner.
POST /mgmt/change-membershipPromote learners to voters / change the voter set.
GET /mgmt/metricsRaft metrics (leader, term, membership, lag).
GET /mgmt/cacheMulti-level cache governor state: budget + per-level/-scope bytes & hit rates (see below).
GET /mgmt/snapshotStream a portable, leader-anchored backup of every database (see below).
GET /mgmt/exportStream one database as an import-compatible CSV tar (see below).
POST /mgmt/restoreRestore a backup into the registry (leader-only, admin; see below).

/raft/append, /raft/vote, /raft/snapshot are the internal peer RPC receivers — not for client use.

GET /mgmt/cache (cache observability)

Reports the node-local, RAM-budget-governed cache hierarchy. Admin-only (it is under /mgmt/). The JSON carries the resolved budget (bytes per region: total, cache_limit, work_mem_limit, headroom, min_free), the overall utilization, a governor block (admission + the free-floor-vs-cap eviction-byte split), the per-levels resident bytes / hit rates with per-scope accounting (which database, collection (label), or query shape is hot), a sankey read-flow block, and the governor’s last free-RAM sample (mem_available_bytes, read from /proc/meminfo). While caching is disabled (the default) it is a truthful all-zeros “just the OS/redb page cache” report:

{
  "enabled": false,
  "budget": { "total": 0, "cache_limit": 0, "work_mem_limit": 0, "headroom": 0, "min_free": 0 },
  "total_resident_bytes": 0,
  "utilization": 0.0,
  "governor": { "admissions": 0, "rejections": 0, "evicted_free_floor_bytes": 0, "evicted_cap_bytes": 0 },
  "levels": [],
  "sankey": { "total_lookups": 0, "misses": 0, "layers": [ { "name": "miss", "label": "miss → storage", "value": 0 } ] },
  "mem_available_bytes": 12884901888
}

An enabled node fills levels (one entry per cache layer, each with bytes/entries/hit_rate/utilization/avg_entry_bytes/evicted_entries/ gen_invalidations/scope_invalidations + hottest-first per-scope scopes rows) and the sankey layers with per-layer absorbed hits.

The same accounting is available in-query as CALL patinadb.cache.stats() YIELD scope, kind, bytes, entries, hit_rate, generation, and as Prometheus gauges on /metrics (patinadb_cache_bytes{level,db}, patinadb_cache_hit_ratio{level}, patinadb_cache_evictions_total, patinadb_mem_available_bytes, …). For the full metric reference and a tuning playbook see Cache Observability & Tuning.

GET /mgmt/snapshot (backup / export)

Streams a portable backup of the whole registry — every database’s graph plus its schema and access-control surface — as a single downloadable JSON document (Content-Type: application/json, Content-Disposition: attachment; filename="patinadb-backup.json").

Query parameters:

ParamDefaultMeaning
historyfalseWhen true, additionally stream each database’s full engram history (delta + snapshot bodies) for point-in-time restore (PITR). See “Point-in-time restore” below.

The backup is built and streamed in O(chunk) memory, not O(graph): each database’s graph is serialized lazily straight from its scan iterators to a temp file (the same streaming serializer the Raft snapshot build uses), and that file is streamed back as the response body and deleted once sent. The payload carries no Raft metadata (no log id / membership), so it is a portable data dump — readable and restorable into any fresh registry:

{
  "databases":         { "<db>": <graph snapshot> },   // graph (HEAD state)
  "fulltext":          { "<db>": [<index def>] },       // full-text indexes
  "vector":            { "<db>": [<index def>] },       // vector indexes (trained centroids)
  "constraints":       { "<db>": [<unique constraint>] },
  "schema_constraints":{ "<db>": [<existence / node-key constraint>] },
  "tags":              { "<db>": [["<name>", "<engram uuid>"]] },  // tag names
  "users":             [<rbac user>],                   // global: hash + per-db roles
  "engram_history":    { "<db>": <full engram history> }, // ONLY with ?history=true
  "heads":             { "<db>": "<engram uuid>" },     // informational
  "taken_at_unix_ms":  <millis>                          // informational
}

So a /mgmt/snapshot/mgmt/restore round-trip reconstitutes each database including its UNIQUE / existence / node-key constraints, compound / vector / full-text indexes, named tags, and the RBAC user directory (argon2 hashes + per-database role grants) — not just the graph. Every schema/RBAC field is optional, so an older backup (from before these were carried) still restores. (restore ignores the heads / taken_at_unix_ms fields — informational labels.)

Point-in-time restore (PITR) — ?history=true

By default (?history=true omitted) the backup is a compact HEAD-state dump: it captures each database’s current graph plus its schema and RBAC, but not its engram delta/snapshot history. After restoring a HEAD-only backup the graph, constraints, indexes, users, and tag names are all back, but time-travel over pre-backup history is gone — AS OF <old-engram-id> / CALL patinadb.diff on engrams that predate the backup are unavailable, and every restored tag resolves to the restored-HEAD engram.

Add ?history=true to make it a PITR archive: the backup additionally streams each database’s full engram history (engram_history above — the per-engram delta bodies and periodic snapshot bodies). A /mgmt/restore of such a backup reconstitutes the whole timeline, so on the restored node:

  • USE <db> AS OF '<old-engram-id>' reconstructs that historical state (not HEAD),
  • USE <db> AS OF TAG <name> resolves to the tagged engram’s point in history,
  • CALL patinadb.diff('<engram-id>') returns that engram’s historical delta,
  • and HEAD is the live restored graph, as always.

The history is streamed body-by-body (one delta / snapshot at a time), so a PITR backup keeps peak memory bounded rather than materialising the whole timeline. The engram_history field is omitted entirely from a HEAD-only backup, and it decodes as empty on restore (#[serde(default)]), so an old backup — or a new HEAD-only one — still restores unchanged.

Cluster caveat. /mgmt/restore installs the engram history on the node that receives the request (the leader). The HEAD graph replicates to followers via Raft, but the delta/snapshot bodies do not — so PITR is a leader-local capability. A follower that later bootstraps purely from a Raft snapshot carries only the metadata summary (the pre-existing engram-log limitation). For a single-node restore (the documented use), full PITR works everywhere.

Leader-anchored, point-in-time labelled

The export takes a leader linearizability barrier (ensure_linearizable) before it reads anything, so:

  • It is leader-only. A follower (or any non-leader) refuses the backup with a 503 naming the current leader ({ "leader_id", "leader_addr", … }), the same contract as a misrouted write or a linearizable read — you can never accidentally take a stale backup from a lagging replica.
  • It reflects every write committed before it began, and records as of what point each database was captured: under the barrier it captures each database’s HEAD engram id (heads[<db>], matching Dataset::head on the leader) plus a wall-clock taken_at_unix_ms.

Consistency caveat (be precise). The per-database graphs are still serialized from their live state, not reconstructed as-of their pinned HEAD. A full as-of reconstruction would have to materialise each graph in memory (O(graph)), which would break the O(chunk) streaming property, so it is intentionally not done here. The practical guarantee is therefore:

  • No stale-follower backups (leader barrier), and each database is labelled with the exact HEAD it was captured at.
  • Cross-database point-in-time consistency holds when writes are quiesced during the export. If writes continue while the (multi-database) backup streams, a write that lands after the barrier can still be included in a database that is serialized later — the recorded per-db heads tell you the intended cut, but the live bytes may run slightly ahead of it. For a guaranteed cross-database snapshot, take the backup from a quiescent cluster.

Auth-protected like every other management route (it is a full data dump): when a password is set, valid HTTP Basic credentials are required.

curl -s -u neo4j:secret http://127.0.0.1:21001/mgmt/snapshot \
  -o patinadb-backup.json

GET /mgmt/export (portable CSV export)

Streams one database’s graph as a portable, import-compatible dump — the online counterpart of the CLI export command.

GET /mgmt/export?db=<name>&format=csv
  • db — which database to export (default default).
  • formatcsv (the only format this release). parquet / arrow return 400; the columnar writers live in the CLI (arrow/parquet deps the server deliberately avoids) and a server columnar export is a documented follow-up.

The response is a tar archive (Content-Type: application/x-tar, Content-Disposition: attachment; filename="patinadb-export-<db>.tar") containing, in the exact neo4j-admin convention the importer reads:

  • nodes_<Label>.csv — a :ID(uuid) column, one typed column per property (age:int, score:float, …), and a trailing :LABEL column.
  • rels_<TYPE>.csv:START_ID(uuid), :END_ID(uuid), :TYPE, and one typed column per edge property.
  • _schema.json — a sidecar carrying the database’s UNIQUE / existence / node-key constraint defs plus compound / full-text / vector index defs, so an exported database can be fully reconstituted. (/mgmt/snapshot now also carries these — see above — so both paths preserve the schema; /mgmt/export is single-database + CSV, /mgmt/snapshot is whole-registry + JSON and additionally carries RBAC users and tags.)

Because the id column is the raw vertex UUID under an :ID(uuid) header, a re-import reproduces the same graph with byte-identical UUIDs (the importer uses the cell literally instead of hashing it). Round-trip:

curl -s -u neo4j:secret \
  "http://127.0.0.1:21001/mgmt/export?db=default&format=csv" -o export.tar
mkdir out && tar xf export.tar -C out
patinadb ./restored import out/nodes_*.csv out/rels_*.csv

Like /mgmt/snapshot, it takes a leader linearizability barrier (so it is leader-only and reflects every committed write; a follower answers 503 with a leader hint) and streams in O(chunk) memory (per-label CSVs are written row-by-row to a temp dir, then tar’d to a temp file that is streamed back and deleted). Auth-protected — it is a /mgmt/ route, so it needs admin credentials.

POST /mgmt/restore (restore / import)

Restores a backup produced by GET /mgmt/snapshot into the registry. Send it to the leader (every step is a replicated write, so a follower answers with a 503 leader hint) and it needs admin credentials (it’s a /mgmt/ route).

curl -s -u neo4j:secret -X POST http://127.0.0.1:21001/mgmt/restore \
  -H 'content-type: application/json' \
  --data-binary @patinadb-backup.json
# → {"databases": 2, "ops_applied": 12345, "users_restored": 3}

For each database in the payload it issues CREATE DATABASE (idempotent), then streams the graph back as chunked replicated writes (so the restore commits through Raft and appears on every node), then re-registers, in order: the full-text index defs, the compound-index defs, the vector-index defs (with their trained centroids), the UNIQUE + existence/node-key constraints (rendered back to CREATE CONSTRAINT … IF NOT EXISTS DDL and replayed via the same replicated path interactive constraint DDL uses), and the tag names (re-created pointing at the restored HEAD). Finally it restores the RBAC user directory — each user (argon2 hash replayed verbatim, so the original password still works) plus its per-database role grants. Every step is a replicated, idempotent write, so a multi-node cluster converges and re-running the same backup is a no-op — ideal for loading a backup into a fresh (empty) cluster. It does not wipe pre-existing data first, so restore into an empty registry (or a database that doesn’t yet hold conflicting ids) for a clean result.

What restore does NOT bring back. The backup is a HEAD-state dump, not a PITR archive (see /mgmt/snapshot above): engram history is not carried, so after a restore, time-travel over pre-backup engrams and pre-backup diffs are gone, and each restored tag resolves to the single restored-HEAD engram (not its original point in history). The anamnesis-enabled flag is not re-toggled on restore (the <db>__anamnesis companion database itself rides the backup as ordinary data, but automatic projection must be re-enabled with CALL patinadb.anamnesis.enable()). The backup is leader-anchored and HEAD-labelled, but each database’s graph is serialized from live state rather than reconstructed as-of its pinned HEAD, so cross-database point-in-time consistency only holds when writes are quiesced during the export.