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

Bolt & Neo4j Browser

The server speaks the native Bolt protocol, so the official Neo4j drivers and the Neo4j Browser connect to patinaDB directly. This has been validated end-to-end against Neo4j driver 6.2.0 (over both raw TCP and WebSocket) and the Neo4j Browser (node creation plus path/relationship graph visualisation).

Connecting

Changing authentication on an existing connection discards its pending cursor and uncommitted explicit transaction. A failed LOGON never retains the previous identity; authenticate successfully again before running queries. LOGOFF is not a way to transfer an open transaction to another user (#1265).

Directory-user sessions also expire when that user’s password, roles or grants change, the user is deleted/recreated, or a snapshot replaces the directory. At the next authenticated message, including PULL or COMMIT, the server discards pending results and uncommitted work and returns Neo.ClientError.Security.Unauthorized. Reconnect and authenticate again (or RESET then LOGON on a protocol version supporting it). Do not retry a permanent permission denial blindly. The check sees locally applied user state; it neither preempts an in-flight message nor makes follower revocations instantaneous. Root-admin and auth-disabled sessions are unaffected (#1265).

The Bolt listener (default 127.0.0.1:7687) auto-detects the transport: a raw Bolt TCP handshake or a WebSocket upgrade (the Browser’s JavaScript driver speaks Bolt inside WebSocket binary frames). Both work on the same port.

Decode resource limits

In the post-0.13.0 audit update (#1274), each decoded message has a shared budget of 128 MiB of requested payload/container allocations and 1,048,576 decoded values, including map keys and the root value. These are policy ceilings, not measurements of total process RSS or a pre-authentication connection-count limit. The existing 64 MiB wire-message ceiling and nesting limit remain separate. Exceeding a decode budget is rejected as a malformed message; split oversized collections across requests instead of retrying the same payload. String/byte payload copies and container reservations share the budget, so nested values do not each receive a fresh allowance.

Completed connection tasks are collected while the listener continues serving new connections (#1272), not retained until process shutdown.

Neo4j Browser: open browser.neo4j.io, connect to bolt://localhost:7687, authenticate with --auth-user / --auth-password.

Official driver (Python):

from neo4j import GraphDatabase
drv = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "secret"))
with drv.session() as s:
    s.run("CREATE (a:Person {name:$n})", n="Ada")
    for rec in s.run("MATCH (n:Person) RETURN n"):
        print(rec["n"])
drv.close()

Nodes, relationships, and paths come back as proper Bolt graph types (Node / Relationship / Path), so drivers hydrate them as graph objects and the Browser visualises them.

Protocol version negotiation: the server advertises Bolt 5.8 down to 5.0, plus 4.4, and negotiates the highest version the client also offers. A client that only speaks an older version still works, with reduced fidelity where that version’s wire format is narrower — for example, a client negotiating Bolt 4.4 (some non-official/community drivers, e.g. Rust’s neo4rs 0.9, only offer 4.4) does not receive the newer element_id field on nodes/relationships; project elementId(n) / elementId(r) in Cypher instead if you need a stable identifier over such a client (the legacy integer id field is a lossy 63-bit hash and is not round-trippable — see Element identity).

Error metadata (GQLSTATUS). A Bolt FAILURE carries the legacy code and message a pre-5.7 driver reads, plus neo4j_code (byte-identical to code), gql_status, and description. neo4j_code is the field a 5.7+ driver uses to populate err.code and decide is_retryable(), so it is the one to branch on and it is exact. gql_status is a best-effort mapping onto the GQL status classes (an unmapped error becomes the general 50N42) and description is derived from the code and message — treat both as diagnostic text, not as a contract. diagnostic_record is deliberately not sent; drivers synthesize their own default. See the Client Retry Contract for which codes are retryable.

Element identity over the wire. For Bolt 5+, a node’s element_id is its UUID string and a relationship’s element_id is the canonical <start>:<end>:<type> handle — both agree byte-for-byte with what Cypher elementId(n) / elementId(r) return, so a value read off a returned entity round-trips as a lookup key. A property named id on a returned node is your own stored property, never the element id (patinaDB does not hijack id); the internal element id is elementId(n) / id(n) / the reserved n.__id. The struct’s legacy integer id field is derived from a lossy hash and must not be used to fetch an entity back.

Spatial values over Bolt

A patinaDB Point is sent as a native Neo4j Point struct, so official drivers decode it as a first-class point (neo4j.spatial.Point / a driver’s Point type):

  • a 2-D point → PackStream struct tag 0x58 with fields {srid: Integer, x: Float, y: Float};
  • a 3-D point → tag 0x59 with an added z: Float.

The srid is the CRS discriminant patinaDB already stores — cartesian 7203, cartesian-3d 9157, wgs-84 4326, wgs-84-3d 4979 (the same SRIDs Neo4j uses). For wgs-84 the point’s x is the longitude and y the latitude.

A point can also be passed as a parameter: send $center as a 0x58/0x59 struct and it decodes back into a point you can use in Cypher, e.g. RETURN distance($center, n.loc).

Polygons have no native Bolt type. Bolt only defines Point2D/Point3D, so a patinaDB Polygon is returned as a Map — {type: "Polygon", srid: Integer, rings: [[[x, y], …], …]} (rings[0] is the exterior ring, rings[1..] the holes). A driver therefore sees a plain map, not a spatial object; decode it yourself if you need the geometry.

Temporal values over Bolt

All six temporal types round-trip as native Bolt temporal structs, in both directions — returned as a driver’s own date/time objects, and accepted as query parameters:

Cypher typeDriver type (Python)
Datedatetime.date
Timedatetime.time (with tzinfo)
LocalTimedatetime.time
DateTimedatetime.datetime (aware)
LocalDateTimedatetime.datetime (naive)
Durationneo4j.time.Duration

DateTime uses the Bolt 5.x encoding when the connection negotiated 5.x and the legacy 4.x encoding on a 4.4 connection, so both driver generations read the same instant.

Before 2026-07 this was silently lossy. A temporal passed as a parameter was converted to a debug string, so it was stored as text rather than a temporal — a data-loss bug that failed silently. If you worked around it by pre-formatting temporals as strings, you can now pass the native values, but any data written through the old path is still text and needs converting.

A parameter carrying a struct patinaDB has no type for is now rejected loudly with Neo.ClientError.Statement.TypeError naming the tag, rather than being coerced to a string. Only Point and the six temporal types are accepted as struct-valued parameters. One documented lossy edge remains: a DateTimeZoneId (a named zone such as Europe/Berlin) decodes to its offset, so the zone name is not preserved.

Authentication

Auth is checked at LOGON (scheme: "basic"). An unauthenticated connection cannot run queries. See Authentication & TLS.

Streaming reads

Read results stream lazily: the query runs on a worker thread that pushes hydrated records over a bounded channel, with PULL n batching and backpressure. Memory stays bounded regardless of result size, so you can stream large result sets without materialising them all server-side.

Transactions

Both autocommit and explicit transactions are real.

Bolt tx_timeout metadata is a non-negative integer in milliseconds: set it on autocommit RUN or explicit BEGIN. A BEGIN deadline is absolute and is not renewed by later RUN messages. Zero omits the client deadline; it does not disable the server’s configured query timeout. The earlier deadline applies, with cooperative cancellation during execution and historical reconstruction (#1261).

  • Autocommit (a bare RUN): each statement commits on its own as one engram — atomic, even for a multi-clause or bulk statement.
  • Explicit (BEGIN … COMMIT / ROLLBACK): statements between BEGIN and COMMIT are buffered, not committed one by one. BEGIN pins a consistent snapshot of the database; every RUN sees that snapshot plus the transaction’s own uncommitted writes (repeatable reads + read-your-own-writes); COMMIT applies the whole buffer as one atomic engram (a single Raft entry on a cluster) after a conflict check; ROLLBACK discards it — nothing is written.

Explicit transactions run at snapshot isolation (SI). A transaction is bound to one database — a mid-transaction USE <other> is rejected — and schema/admin statements (e.g. CREATE INDEX, CREATE DATABASE) run eagerly, not buffered. The official Neo4j drivers’ managed transaction functions (session.execute_write/executeWrite, or equivalent) work as expected, including automatic retry on the conflict error below — but a plain autocommit RUN outside a managed transaction is never retried by any driver, in any version. If your backend issues autocommit writes under contention, it must implement its own retry loop (see Client Retry Contract); this is independent of the conflict-code fix below and true regardless of which patinaDB version you run.

Isolation: explicit transactions are snapshot-isolated, not serializable

  • BEGIN pins the database HEAD (the serialization point). Every read in the transaction observes the graph as of that moment — a commit by another connection made after your BEGIN is invisible to your transaction (repeatable reads, no phantoms). Your own buffered writes are visible to your own reads (read-your-own-writes).
  • COMMIT is first-committer-wins. Before proposing the buffer, the server checks whether any write committed since your pinned snapshot touched an entity/property your transaction also wrote. If so, the COMMIT is rejected with a transient error (Neo.TransientError.General.TransientError), which every official Neo4j driver’s managed-transaction function retries automatically (session.execute_write/executeWrite, or equivalent). This prevents lost updates (e.g. two clients that both “read a counter, increment, write it back” — one commits, the other retries against the new value instead of silently clobbering it). A losing leadership mid-COMMIT surfaces the same driver-retryable code, for the same reason: it too is a routine event a well-behaved client simply retries.
  • Autocommit (a bare single-statement RUN) holds the per-database write lock for the statement, so autocommit writes to one database are serialized on the server side — but a plain autocommit RUN is never automatically retried by the driver itself, in any version, since it isn’t wrapped in a managed-transaction function. A client issuing autocommit writes under contention must retry them at the application level.

What SI does not give you: write skew

Snapshot isolation detects write-write conflicts only. A read-write conflict with disjoint write-sets (write skew) is still possible: two transactions can each read a value the other overwrites, as long as they write different things, and both commit. This is the standard, well-understood SI limitation — it is not serializable and not linearizable. If you need a constraint that spans rows one transaction reads and another writes (e.g. “at most one on-call engineer”), enforce it in a single autocommit statement or with an application-level guard. See Limitations.

Conflict granularity

Property writes conflict at (entity, property) granularity (two transactions updating different properties of the same node do not falsely conflict); create/delete and label changes conflict at whole-entity granularity. Freshly-created nodes carry new UUIDs, so concurrent inserts never conflict.

Idempotency tokens (idempotency_key in tx_metadata)

A write may carry a client-chosen idempotency token so that a retry after an ambiguous outcome (lost response, failover mid-commit) is recognised as an already-committed duplicate instead of applied twice. Set it in the transaction metadata under the reserved key idempotency_key:

  • Autocommit: in the RUN’s own metadata (the tx_metadata argument of session.run(...)).
  • Explicit transaction: in BEGIN’s metadata — session.begin_transaction(metadata={"idempotency_key": …}). The token is a property of the whole transaction and is checked once, at COMMIT; a RUN’s own metadata inside an already-open transaction is not read for this. An invalid token fails BEGIN loudly.

Token rules: printable ASCII without spaces, non-empty, ≤ 128 bytes; you own uniqueness — reusing one token for two different writes silently loses the second. A recognised duplicate is a success that applied nothing; its write counters are all zero, and the response names the original’s log index in duplicate_of. dedup_active only tells you the check ran — it is not the duplicate signal.

Observability caveat for explicit transactions. The dedup_active / duplicate_of fields ride the COMMIT message’s SUCCESS metadata, which the official Python driver behind session.execute_write does not expose to application code — the ResultSummary you can reach belongs to the buffered statements and reports dedup_active: false. The protection works; you just cannot see it over that API. If you must observe the dedup decision, use an autocommit statement (its terminal SUCCESS is reachable) or the REST surface. Full contract: Client Retry Contract → Idempotency tokens.

Side-effect counters (ResultSummary.counters)

Every write’s Bolt SUCCESS metadata now carries a stats map — the same Neo4j wire keys the official driver hydrates into ResultSummary.counters (the wire keys are hyphenated — nodes-created, nodes-deleted, relationships-created, relationships-deleted, properties-set — and a driver surfaces them as counters.nodes_created and so on; contains_updates is derived client-side from any non-zero counter, so it is never sent explicitly). Only non-zero counters are sent. It is folded from the already-resolved DeltaOp batch for the statement (or, for an explicit BEGIN … COMMIT transaction’s COMMIT, the whole transaction’s accumulated batch) — so it reflects exactly what was applied, not a guess — and is omitted entirely for a pure read (no stats key at all, matching Neo4j: a read never “contains updates”).

Two counting decisions worth knowing:

  • properties_set covers both assignment and removal. Neo4j has one property counter, not separate set/removed ones, and a SET n.p = null removes the key in patinaDB’s model — so a SET, a SET n += {...}, and a REMOVE n.p all increment properties_set.
  • labels_added/labels_removed are never emitted. SET n:Label / REMOVE n:Label carry the vertex’s full post-batch secondary-label set in the resolved op, not a delta against what it held before, so there is no way to derive “how many labels were added” from the op alone without re-reading prior state. An absent counter (the driver’s own default of 0) is preferable to a fabricated number.

labels_added/labels_removed, indexes_added/indexes_removed, constraints_added/constraints_removed, and system_updates are not populated (DDL isn’t tracked by this mechanism yet); a write that only changes labels or schema will still report contains_updates correctly via properties_set/nodes_created/etc. where applicable, but won’t itself set those specific counters.

Causal consistency (bookmarks)

A standard Neo4j driver session gets read-your-writes automatically: the driver threads an opaque bookmark from each statement’s result into the next session.run call, and the server blocks that next read until it has caught up to the bookmark. This now actually works end-to-end against patinaDB — no client code change needed, since it’s the driver’s default session behaviour.

Concretely:

  • Every successful autocommit write (a bare RUN) and every successful COMMIT returns a bookmark encoding the Raft log index that write was committed at.
  • A subsequent BEGIN or autocommit RUN carrying that bookmark blocks until this connection’s node has applied that index, before doing anything else — for BEGIN, before it pins its snapshot-isolation snapshot, so the pinned snapshot itself already reflects the bookmarked write.
  • Once caught up (the common case, especially against the leader — no wait at all), the statement runs as normal.

If BEGIN cannot open its snapshot-isolation base before creating a transaction, it returns Neo.TransientError.General.DatabaseUnavailable with patinadb_effect: "none". After a successful RESET, the client may retry that BEGIN with the same database and bookmarks. A missing no-effect receipt, a transport failure, or a failed COMMIT does not establish that retrying a write is safe. Snapshot-open failures never fall back to weaker constraint enforcement.

The message tells the two causes apart. When a concurrent write is publishing (normal under write load), the message ends with a complete native read snapshot is unavailable; retry the read. Any other cause ends with internal server error, and its detail stays in the server log. Both carry the same code and patinadb_effect: "none", so the retry rule above holds for both. The same publication message is returned when a RUN read is refused for this reason, matching REST’s 503 READ_PUBLICATION_IN_PROGRESS body.

This matters most when a routing (neo4j://) driver load-balances a session’s reads across followers (see cluster-aware routing below): without a bookmark, a write-then-read session could land its read on a follower that hasn’t replicated the write yet and get a stale result with no error. With bookmarks, that same session either waits briefly for the follower to catch up, or times out loudly — it never silently returns stale data.

The wait is bounded: --bookmark-wait-timeout-secs (default 10 seconds; see Configuration Reference) caps how long a node will wait for its own applied state to catch up to an incoming bookmark. If that bookmark can’t be reached in time, the client gets a driver-retryable Neo.TransientError.Transaction.BookmarkTimeout. A managed transaction may retry, and routing may select a more caught-up eligible member; bare session.run() does not provide that managed retry loop — never substitute an unbookmarked read. Set --bookmark-wait-timeout-secs 0 disables legacy-index waiting only. Scoped tokens never bypass their barrier; compatible/strict mode requires a positive timeout.

Scoped bookmarks and rolling upgrades

--causal-bookmark-mode legacy is the compatibility default. compatible emits neo4j:patinadb:v2:<domain-uuid>:<index> and also accepts retained legacy tokens; strict rejects legacy Patina tokens. Malformed recognized tokens and foreign causal domains fail explicitly. Unrelated vendors’ bookmark namespaces are ignored. Upgrade every member before enabling scoped mode and enable it on every intended client endpoint. Initialization commits one shared domain through Raft. Election, restart and same-cluster snapshot onboarding preserve it. Domain initialization is not reversible through normal configuration; binary downgrade afterwards is unsupported. Never send scoped clients to an old binary that ignores the format.

An Engram UUID identifies history, not an applied prefix. Historical bodies may still be backfilling when a current-state read satisfies a scoped bookmark. Across sessions, pass bookmarks explicitly or use the driver’s bookmark manager; independent sessions without shared dependencies are not causally linked.

A history-only AS OF / request-at read still passes the causal admission barrier, but deliberately returns the requested historical state. Its summary preserves only incoming dependencies (index 0 if absent), not the newer current applied position. Mixed historical/current-read transactions retain their conservative BEGIN frontier; writes retain their committed effect position and incoming dependencies.

Python backend: routing, retries and request dependencies

Use one shared driver but a separate session per request/thread. In a routed cluster, select READ_ACCESS/execute_read for reads; the default write mode is not a request to load-balance over regional readers. Across HTTP backend instances, serialize the driver’s opaque bookmark strings and pass them into the next session. Retain independent workers’ frontiers separately, and combine them when joining their work; do not parse or numerically compare tokens.

For a driver created with a bounded max_transaction_retry_time, a read can use:

from neo4j import Bookmarks, READ_ACCESS, unit_of_work

@unit_of_work(timeout=5)
def read_ticket(transaction, ticket_id):
    result = transaction.run(
        "MATCH (ticket:Ticket {id: $id}) RETURN ticket.title AS title",
        id=ticket_id,
    )
    return [record.data() for record in result]

with driver.session(
    database="default",
    default_access_mode=READ_ACCESS,
    bookmarks=Bookmarks.from_raw_values(incoming_bookmarks),
) as session:
    rows = session.execute_read(read_ticket, requested_id)
    outgoing_bookmarks = list(session.last_bookmarks().raw_values)

The timeout belongs on unit_of_work for managed transactions or on begin_transaction(timeout=...) for explicit ones. Autocommit uses session.run(Query(text, timeout=...)); transaction.run takes query text, not a Query object. Passing timeout as an ordinary run parameter does not configure the transaction deadline. See the official Python driver’s transaction API and bookmark guidance.

Consume results inside the callback and avoid external side effects: managed callbacks may execute more than once. Retry budgets do not replace socket, connection-acquisition and application deadlines. local-only cannot fall back to a writer outside the selected region; if its readers cannot satisfy a bookmark, surface bounded unavailability instead of silently weakening consistency.

An older bookmark cannot settle a lost commit acknowledgement. A later read may observe the write, but its absence does not prove the write was aborted. For safe write retries, use the documented idempotency-token contract or explicitly surface an unknown outcome; do not blindly repeat an increment.

Read-consistency by surface

Different ways of talking to patinaDB give different freshness guarantees for a read that follows a write:

SurfaceGuarantee
Bolt driver session (bookmarks)Read-your-writes — automatic, bounded-wait (above). No client change needed.
REST POST /cypher, "consistency":"local" (default)Eventually consistent on a follower; fast, no wait.
REST POST /cypher, "consistency":"linearizable"Read-your-latest-write against live HEAD — leader-only, costs a round-trip. See REST API.
A plain follower read with no bookmark / no linearizable flagEventually consistent — bounded staleness. The neo4j:// routing table already excludes chronically-lagging followers (see High Availability).

See High Availability → Read consistency for the full discussion, including the lag-immune AS OF TAG pattern for reads that must be reproducible across followers with no leader round-trip at all.

Connection resource limits (unauthenticated slowloris hardening)

Three coordinated guards close an unauthenticated connection-exhaustion attack (issue #440): an attacker who opens Bolt sockets and either sends nothing, stalls partway through the version-negotiation handshake, or completes HELLO/LOGON and then idles, used to hold a --max-bolt-connections permit indefinitely — a few thousand such connections was a complete driver-protocol lockout, not merely file-descriptor pressure.

  • --bolt-handshake-timeout-secs (PATINADB_BOLT_HANDSHAKE_TIMEOUT_SECS, default 10): bounds every pre-authentication step an accepted socket can stall on — the TLS ClientHello (--tls-cert), the raw-Bolt-vs-WebSocket dispatch first-byte peek, the WebSocket HTTP upgrade (the Neo4j Browser transport), and the 20-byte Bolt version-negotiation handshake. So on all three transports (plain TCP, TLS, WebSocket) a socket that sends nothing at all, stalls partway through the TLS/WebSocket handshake, or sends fewer than 20 Bolt handshake bytes and then stalls, is closed cleanly once this deadline elapses instead of pinning the connection task forever. Set 0 to disable.
  • --bolt-idle-timeout-secs (PATINADB_BOLT_IDLE_TIMEOUT_SECS, default 300): bounds how long a connection may sit with no open explicit transaction and no started-but-unconsumed result stream — those two states have their own dedicated reapers, --idle-tx-timeout-secs and --stream-pull-timeout-secs below — while waiting for the next client message. Covers a connection that finishes the handshake and/or HELLO/LOGON and then goes silent. The clock resets on every client message. An idle connection closes without an unsolicited Bolt response when this timer expires. A connection in an otherwise healthy driver pool can legitimately expire; applications should validate pooled connections before reusing them after a long idle period. For the Neo4j Python driver, configure liveness_check_timeout (the sample backend uses 60 seconds; 0 checks every reuse). This checks the socket before dispatch, not replaying a write whose commit outcome is unknown. See the driver configuration reference. Server value 0 disables this idle reaper; disabling resource guards is not required for pooling.
  • The --max-bolt-connections permit is acquired only after a successful LOGON (or, with --insecure-disable-auth, on a connection’s first message — auth-disabled connections have no separate LOGON event to key off of), never at TCP accept time. An unauthenticated connection — silent, stalled mid-handshake, or idling after HELLO — therefore never consumes the connection budget meant for real, authenticated clients; it is bounded purely in time by the two deadlines above. A connection that is over the cap at the moment it would authenticate gets a Neo.TransientError.General.DatabaseUnavailable failure (driver-retryable) in place of its normal authentication success, and the socket is closed.

Transaction resource limits

Two operational guards bound an explicit transaction so a runaway or abandoned one can’t exhaust server resources (both default ON, opt out with 0):

  • --max-tx-ops (PATINADB_MAX_TX_OPS, default 5000000): the maximum number of resolved write operations an explicit transaction may buffer across all its statements before COMMIT. The buffer commits as one Raft entry, so this bounds connection RAM and the entry size. Exceeding it aborts and rolls back the transaction with a driver-retryable transient error — split a very large load into smaller transactions (or use CALL { … } IN TRANSACTIONS, which commits in bounded chunks). Each individual statement is separately bounded by PATINADB_MAX_CAPTURE_OPS.
  • --idle-tx-timeout-secs (PATINADB_IDLE_TX_TIMEOUT_SECS, default 300): the “idle in transaction” guard. An explicit transaction that has been open with no client activity for this long is rolled back and its connection is closed by a background reaper — so an abandoned BEGIN (a client that opened a transaction and then went away without COMMIT/ROLLBACK) can’t hold resources forever. The clock resets on every statement, so a slow-but-active transaction is never reaped. Set 0 to disable the reaper.

Operator termination through /mgmt/transactions/{id}/terminate also wakes an idle transaction owner, rolls back its pending work and closes its connection. This does not depend on the idle reaper being enabled. Active work continues to observe the cooperative termination flag.

Database selection

The target database is taken per-RUN / per-BEGIN (from the Bolt db field or a USE <db> prefix) and is never sticky across autocommit statements — connections are pooled, so each statement resolves its own database. See Multi-Database.

System / introspection shim

On connect, the Neo4j Browser fires admin/introspection statements that are not ordinary Cypher (CALL dbms.components(), SHOW DATABASES, CALL db.labels(), routing-table lookups, …). The server recognises these and returns canned/registry-derived results so the Browser connects cleanly, shows a server version, and populates its database dropdown.

Anything the engine actually implements reaches the engine. The shim never shadows a real feature: CALL db.labels() / db.relationshipTypes() / db.propertyKeys(), CALL db.index.fulltext.*, CALL db.index.vector.*, and the whole SHOW family the engine supports — SHOW CONSTRAINTS, SHOW INDEXES, SHOW POINT INDEXES, SHOW EDGE SORTED INDEXES, SHOW FUNCTIONS — all pass straight through and return real data. (Until 2026-07 the shim swallowed the SHOW family and answered empty-success, so SHOW CONSTRAINTS over Bolt reported no constraints on a database that had them. It now reports them.)

Still canned: SHOW DATABASES (answered from the registry), SHOW PROCEDURES (returns an empty list — the engine has no procedure catalogue), and the CALL dbms.* introspection the Browser needs.

Cluster-aware routing

neo4j:// (routing) drivers ask the server for a routing table and then connect to the addresses it returns. In a cluster, patinaDB answers with the real topology — writes to the leader, reads spread across the followers — so a routing driver offloads reads to replicas automatically. See High Availability → Cluster-aware Bolt routing for the full WRITE/READ/ROUTE breakdown and the lag-immune clean-tag read pattern. A single-node leader returns itself for all three roles. During leader discovery, WRITE may be empty with a short-lived table; a follower is not a substitute writer. Native ROUTE honors the driver’s requested database (including the neo4j alias), waits for recognized causal bookmarks, then checks closed-mode database authorization against the catalog. Invalid ROUTE bookmark framing returns an argument error; invalid Patina bookmark token contents return a bookmark error. Bolt user impersonation is not implemented; a ROUTE request carrying imp_user fails explicitly instead of silently routing as the authenticated service account.

Behind a reverse proxy

Behind a TLS-terminating proxy or load balancer, set --advertised-addr to the public host:port so the routing table sends drivers to a reachable endpoint (each node advertises its own). For a direct bolt:// connection this isn’t needed — the default advertises the listen address. See Authentication & TLS.