Connecting with Neo4j drivers (Bolt)
Fluree serves its openCypher surface over the Bolt protocol, so official Neo4j drivers and Bolt-compatible tools connect directly. This guide covers enabling the listener and connecting from driver code; protocol details live in the Bolt reference.
Enable the listener
Bolt is compiled in by default but binds only when an address is configured (conventional port 7687):
fluree-server \
--bolt-listen-addr 0.0.0.0:7687 \
--bolt-default-db mydb:main
The CLI's server management exposes the same flags on run, start,
and restart (background servers keep them across restarts, and
fluree server status shows the Bolt address):
fluree server start --bolt-listen-addr 0.0.0.0:7687 --bolt-default-db mydb:main
or in the config file:
[server.bolt]
listen_addr = "0.0.0.0:7687"
default_db = "mydb:main"
| Setting | Flag / Env | Meaning |
|---|---|---|
listen_addr | --bolt-listen-addr / FLUREE_BOLT_LISTEN_ADDR | Address to serve Bolt on. Unset = disabled. |
default_db | --bolt-default-db / FLUREE_BOLT_DEFAULT_DB | Ledger served to sessions that select no database. |
Sessions pick their ledger with the driver's standard database
selection; default_db is the fallback. A session with neither fails
its queries with a clear error.
Authentication follows the server's data_auth_mode, exactly like the
HTTP data plane: none (default) runs open, optional verifies tokens
when presented, required refuses anonymous sessions. Front with a TCP
proxy for TLS if the wire crosses trust boundaries — tokens on a bare
TCP wire are visible to the network.
Authenticate
Pass the same data-plane token the HTTP API accepts (DID-JWS or OIDC JWT) as a bearer credential — there is no separate Bolt user store:
from neo4j import GraphDatabase, AuthTokens
driver = GraphDatabase.driver("bolt://localhost:7687",
auth=AuthTokens.bearer(token))
# Driver code shaped as user/password works too: the password field
# carries the token and the username is ignored.
driver = GraphDatabase.driver("bolt://localhost:7687",
auth=("token", token))
The token's ledger scopes (fluree.ledger.read.* / .write.*) gate
every statement, its identity (fluree.identity claim) drives
data policies — two sessions with different
tokens can see different graphs from the same Cypher — and expiry is
re-checked per statement (Neo.ClientError.Security.TokenExpired
tells 5.x drivers to re-authenticate). See the
Bolt reference for details.
Names
Labels, relationship types, and property keys are plain names, exactly
as a Neo4j user expects — CREATE (n:Person {name: "Ada"}) stores and
matches the names Person and name with no namespace involved. If
the ledger also holds RDF-style data (full IRIs), configure the
ledger's default context with @vocab so bare Cypher names resolve
into that vocabulary — see
names and IRIs.
Connect (Python)
from neo4j import GraphDatabase
# bolt:// (direct) scheme — neo4j:// routing is not served.
# auth=None works when the server doesn't require data auth.
driver = GraphDatabase.driver("bolt://localhost:7687", auth=None)
with driver.session(database="mydb:main") as session:
# Autocommit query with parameters.
result = session.run(
"MATCH (p:Person) WHERE p.age > $min RETURN p.name AS name",
min=30,
)
for record in result:
print(record["name"])
# Nodes come back typed: labels + properties + elementId (the durable identity).
node = session.run("MATCH (p:Person) RETURN p LIMIT 1").single()["p"]
print(node.labels, dict(node), node.element_id)
# Managed transaction function — retried automatically on
# concurrent-write conflicts.
def add_person(tx):
tx.run("CREATE (n:Person {name: $name})", name="Ada").consume()
return tx.run("MATCH (n:Person) RETURN count(n) AS c").single()["c"]
count = session.execute_write(add_person)
Connect (JavaScript)
const neo4j = require("neo4j-driver");
const driver = neo4j.driver("bolt://localhost:7687");
const session = driver.session({ database: "mydb:main" });
const result = await session.executeWrite(async (tx) => {
await tx.run("CREATE (n:Person {name: $name})", { name: "Ada" });
const res = await tx.run("MATCH (n:Person) RETURN count(n) AS c");
return res.records[0].get("c");
});
await session.close();
Transactions and retries
Explicit transactions are optimistic: statements execute against the
state the transaction began on (with read-your-writes), and COMMIT
succeeds only if no concurrent write landed in between. On conflict the
driver receives a Neo.TransientError.* failure — use transaction
functions (execute_read / execute_write / executeQuery), which
retry automatically; hand-rolled begin_transaction() code must be
prepared to retry on transient errors. Explicit transactions require a
single-node server; Raft/peer deployments reject BEGIN (autocommit
works everywhere).
Troubleshooting
- Connection refused — the listener only binds when
bolt_listen_addris set; check the startup log forBolt listener starting. - "unsupported protocol version" / immediate close — the driver proposed only versions outside 4.4/5.0–5.4 (very old drivers). Upgrade the driver.
neo4j://scheme fails — usebolt://; server-side routing tables are not served.- AuthError /
Security.Unauthorized— the server runsdata_auth_mode=requiredand the session presented no (or an invalid) token; connect withAuthTokens.bearer(<data-plane token>). - Queries fail with
DatabaseNotFoundon a ledger that exists — the token's ledger scopes don't cover it (out-of-scope access is reported as not-found to avoid leaking ledger existence). Security.TokenExpiredmid-session — the bearer token outlived itsexp; re-authenticate (5.x drivers with an auth-token manager do this automatically) or reconnect with a fresh token.- BEGIN fails with "single-node server" — the deployment replicates writes (Raft) or is a peer; use autocommit queries.
- Numbers look different from the JSON API —
xsd:decimalis rendered as Float over Bolt (Neo4j parity); the JSON transport keeps exact decimal strings. See Bolt value mapping. - Bare
MATCH (n)rejected — whole-graph scans are opt-in viaFLUREE_CYPHER_ALLOW_FULL_SCAN=1, same as over HTTP.