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
0x58with fields{srid: Integer, x: Float, y: Float}; - a 3-D point → tag
0x59with an addedz: 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 type | Driver type (Python) |
|---|---|
Date | datetime.date |
Time | datetime.time (with tzinfo) |
LocalTime | datetime.time |
DateTime | datetime.datetime (aware) |
LocalDateTime | datetime.datetime (naive) |
Duration | neo4j.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 betweenBEGINandCOMMITare buffered, not committed one by one.BEGINpins a consistent snapshot of the database; everyRUNsees that snapshot plus the transaction’s own uncommitted writes (repeatable reads + read-your-own-writes);COMMITapplies the whole buffer as one atomic engram (a single Raft entry on a cluster) after a conflict check;ROLLBACKdiscards 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
BEGINpins 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 yourBEGINis invisible to your transaction (repeatable reads, no phantoms). Your own buffered writes are visible to your own reads (read-your-own-writes).COMMITis 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, theCOMMITis 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 autocommitRUNis 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 (thetx_metadataargument ofsession.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, atCOMMIT; aRUN’s own metadata inside an already-open transaction is not read for this. An invalid token failsBEGINloudly.
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_setcovers both assignment and removal. Neo4j has one property counter, not separate set/removed ones, and aSET n.p = nullremoves the key in patinaDB’s model — so aSET, aSET n += {...}, and aREMOVE n.pall incrementproperties_set.labels_added/labels_removedare never emitted.SET n:Label/REMOVE n:Labelcarry 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 successfulCOMMITreturns a bookmark encoding the Raft log index that write was committed at. - A subsequent
BEGINor autocommitRUNcarrying that bookmark blocks until this connection’s node has applied that index, before doing anything else — forBEGIN, 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:
| Surface | Guarantee |
|---|---|
| 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 flag | Eventually 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, default10): 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. Set0to disable.--bolt-idle-timeout-secs(PATINADB_BOLT_IDLE_TIMEOUT_SECS, default300): 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-secsand--stream-pull-timeout-secsbelow — 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, configureliveness_check_timeout(the sample backend uses 60 seconds;0checks every reuse). This checks the socket before dispatch, not replaying a write whose commit outcome is unknown. See the driver configuration reference. Server value0disables this idle reaper; disabling resource guards is not required for pooling.- The
--max-bolt-connectionspermit 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 aNeo.TransientError.General.DatabaseUnavailablefailure (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, default5000000): the maximum number of resolved write operations an explicit transaction may buffer across all its statements beforeCOMMIT. 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 useCALL { … } IN TRANSACTIONS, which commits in bounded chunks). Each individual statement is separately bounded byPATINADB_MAX_CAPTURE_OPS.--idle-tx-timeout-secs(PATINADB_IDLE_TX_TIMEOUT_SECS, default300): 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 abandonedBEGIN(a client that opened a transaction and then went away withoutCOMMIT/ROLLBACK) can’t hold resources forever. The clock resets on every statement, so a slow-but-active transaction is never reaped. Set0to 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.