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
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.
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.
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.
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.
- 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 work as expected, including
automatic retry on the conflict error below.
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.Transaction.LockClientStopped), which the Neo4j drivers’ managed-transaction functions retry automatically. 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).- Autocommit (a bare single-statement
RUN) holds the per-database write lock for the statement, so autocommit writes to one database are serialized.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.
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. Real procedures (notably
CALL db.index.fulltext.*) are not swallowed by the shim — they reach the
engine and return real data.
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 server returns itself for all three roles.
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.