Neo4j BOLT Protocol
|
BOLT protocol support is available starting from ArcadeDB version 26.2.1. |
ArcadeDB Server supports the Neo4j BOLT protocol, enabling connectivity from any BOLT-compatible client or driver. This allows you to use the official Neo4j drivers with ArcadeDB, leveraging the native OpenCypher query engine for graph operations.
The BOLT implementation is certified against all five official Neo4j drivers (Java, JavaScript, Python, .NET, and Go) through a shared conformance spec. See the Driver Compatibility Matrix for the exact, per-driver certification status.
The BOLT protocol implementation supports:
-
BOLT v3.0, v4.0, v4.4, and v5.0–v5.4 protocol versions, negotiated automatically with the client
-
Full Cypher query support via ArcadeDB’s native OpenCypher implementation
-
Parameterized queries for security and performance
-
TLS/SSL encrypted connections (
bolt+s://,neo4j+s://) -
Explicit and managed transactions, including automatic retry on transient errors
-
Bookmarks for causal consistency across sessions
-
Multi-database support with database selection per session
-
HA-aware routing so
neo4j://discovery reflects the actual cluster topology -
Native BOLT structures for graph (Node, Relationship, Path), temporal,
Duration, and spatialPointvalues -
Multi-label vertices following Neo4j’s node labeling conventions
Setup
If you’re using ArcadeDB as embedded, add the dependency to the arcadedb-bolt library.
If you’re using Maven, include this dependency in your pom.xml file:
<dependency>
<groupId>com.arcadedb</groupId>
<artifactId>arcadedb-bolt</artifactId>
<version>26.5.1</version>
</dependency>
To start the BOLT plugin, enlist it in the server.plugins settings.
To specify multiple plugins, use the comma , as separator.
Example:
~/arcadedb $ bin/server.sh -Darcadedb.server.plugins="Bolt:com.arcadedb.bolt.BoltProtocolPlugin"
If you’re using MS Windows OS, replace server.sh with server.bat.
In case you’re running ArcadeDB with Docker, use -e to pass settings and open the BOLT default port 7687:
docker run --rm -p 2480:2480 -p 2424:2424 -p 7687:7687 \
--env JAVA_OPTS="-Darcadedb.server.rootPassword=playwithdata \
-Darcadedb.server.plugins=Bolt:com.arcadedb.bolt.BoltProtocolPlugin " \
arcadedata/arcadedb:latest
The Server output will contain this line:
INFO [ArcadeDBServer] - Bolt Protocol plugin started (host=0.0.0.0 port=7687)
Configuration
The BOLT plugin supports the following configuration options:
| Setting | Default | Description |
|---|---|---|
|
7687 |
TCP/IP port for BOLT connections |
|
0.0.0.0 |
Host/IP address to bind to |
|
(none) |
Default database when not specified by the client; if unset, the first available database is used |
|
0 |
Maximum concurrent connections (0 = unlimited) |
|
DISABLED |
TLS mode for BOLT connections: |
|
300 |
Time-to-live in seconds for routing table entries returned to |
|
false |
Enable BOLT protocol debug logging |
Example configuration:
bin/server.sh -Darcadedb.server.plugins="Bolt:com.arcadedb.bolt.BoltProtocolPlugin" \
-Darcadedb.bolt.port=7687 \
-Darcadedb.bolt.defaultDatabase=mydatabase
TLS/SSL
The BOLT plugin supports encrypted connections. Set arcadedb.bolt.ssl to one of:
| Mode | Behavior |
|---|---|
|
(default) plaintext only. Clients connect with |
|
the server auto-detects the connection: a TLS handshake is served encrypted, a plaintext connection is served in the clear. Useful during migration. |
|
only encrypted connections are accepted. Clients must use |
When TLS is enabled (OPTIONAL or REQUIRED), the plugin reuses the server’s shared HTTPS keystore and truststore. Configure them with the standard network settings:
bin/server.sh -Darcadedb.server.plugins="Bolt:com.arcadedb.bolt.BoltProtocolPlugin" \
-Darcadedb.bolt.ssl=REQUIRED \
-Darcadedb.network.ssl.keyStore=/path/to/keystore.p12 \
-Darcadedb.network.ssl.keyStorePassword=secret \
-Darcadedb.network.ssl.trustStore=/path/to/truststore.jks \
-Darcadedb.network.ssl.trustStorePassword=secret
The keystore defaults to PKCS12 and the truststore to JKS. Connect from a driver using the bolt+s:// (or neo4j+s:// for routing) scheme.
Server Identity and Feature Envelope
Neo4j drivers gate feature negotiation off the server identity they receive during the initial handshake. ArcadeDB deliberately advertises a Neo4j-compatible identity so that the official drivers enable the correct feature set:
-
In the
HELLO/SUCCESSresponse the server reports itself asNeo4j/5.26.0 compatible (ArcadeDB <version>). -
CALL dbms.components()returnsNeo4j Kernel, version5.26.0, editioncommunity.
This advertised identity is the contract against which every certification result is measured: it declares the Neo4j 5.26-era feature envelope the drivers may assume. The following capabilities are certified as working against the official drivers:
-
Protocol negotiation for BOLT 3.0/4.0/4.4 and 5.0–5.4.
-
Basic authentication.
-
Autocommit, explicit (
BEGIN/COMMIT/ROLLBACK), and managed transactions with retry-on-transient. -
Bookmarks (causal consistency) and per-session database selection.
-
Streaming (
PULL/DISCARD) with accurate result-summary counters. -
Native BOLT structures for Node, Relationship, Path, temporal types,
Duration, spatialPoint, byte arrays, and nested lists/maps. -
Structured
Neo.ClientError.andNeo.TransientError.error codes. -
HA-aware
neo4j://routing.
Explicit, documented differences from a real Neo4j server (never silent omissions):
-
The
noneauthentication scheme is intentionally rejected — ArcadeDB always requires credentials. -
RIDandUUIDvalues are serialized as strings (there is no native BOLT type for them). -
BigDecimaland out-of-rangeBigIntegervalues are down-converted todouble, which may lose precision. -
Aura and enterprise-clustering-specific driver behaviors are out of scope.
-
The advertised
server_agentversion (5.26.0) is a fixed, deliberate choice; changing it is a compatibility-affecting decision.
Compatible Drivers
The Neo4j official drivers are open source and licensed under Apache 2.0. You can use them with ArcadeDB’s BOLT protocol implementation:
| Language | Driver | Installation |
|---|---|---|
Java |
Maven: |
|
Python |
|
|
JavaScript |
|
|
.NET |
NuGet: |
|
Go |
|
Each driver is certified across a pinned version range that is re-tested nightly against driver-side releases. The exact versions are tracked in the driver-version matrix; see the Driver Compatibility Matrix for per-scenario results.
For the complete list of community drivers, check the Neo4j Driver documentation.
Connection URI Schemes
| Scheme | Meaning |
|---|---|
|
Direct connection to a single server, plaintext. |
|
Direct connection to a single server, TLS-encrypted. |
|
Routing/discovery connection — the driver asks the server for the cluster routing table (HA-aware). |
|
Routing/discovery connection, TLS-encrypted. |
Java Example
import org.neo4j.driver.*;
// Create driver (without encryption for local development)
Driver driver = GraphDatabase.driver(
"bolt://localhost:7687",
AuthTokens.basic("root", "playwithdata"),
Config.builder().withoutEncryption().build()
);
// Open session for specific database
try (Session session = driver.session(SessionConfig.forDatabase("mydatabase"))) {
// Execute a simple query
Result result = session.run("MATCH (n:Person) RETURN n.name AS name LIMIT 10");
while (result.hasNext()) {
Record record = result.next();
System.out.println(record.get("name").asString());
}
// Execute parameterized query
Result paramResult = session.run(
"MATCH (p:Person) WHERE p.age >= $minAge RETURN p.name, p.age",
Values.parameters("minAge", 25)
);
// Execute in explicit transaction
try (Transaction tx = session.beginTransaction()) {
tx.run("CREATE (n:Person {name: $name, age: $age})",
Values.parameters("name", "Alice", "age", 30));
tx.run("CREATE (n:Person {name: $name, age: $age})",
Values.parameters("name", "Bob", "age", 25));
tx.commit();
}
}
driver.close();
Python Example
from neo4j import GraphDatabase
# Create driver
driver = GraphDatabase.driver(
"bolt://localhost:7687",
auth=("root", "playwithdata")
)
# Query example
with driver.session(database="mydatabase") as session:
# Simple query
result = session.run("MATCH (n:Person) RETURN n.name AS name LIMIT 10")
for record in result:
print(record["name"])
# Parameterized query
result = session.run(
"MATCH (p:Person) WHERE p.age >= $minAge RETURN p.name, p.age",
minAge=25
)
# Explicit transaction
with session.begin_transaction() as tx:
tx.run("CREATE (n:Person {name: $name, age: $age})", name="Alice", age=30)
tx.run("CREATE (n:Person {name: $name, age: $age})", name="Bob", age=25)
tx.commit()
driver.close()
JavaScript Example
const neo4j = require('neo4j-driver');
// Create driver
const driver = neo4j.driver(
'bolt://localhost:7687',
neo4j.auth.basic('root', 'playwithdata')
);
// Query example
const session = driver.session({ database: 'mydatabase' });
try {
// Simple query
const result = await session.run('MATCH (n:Person) RETURN n.name AS name LIMIT 10');
result.records.forEach(record => {
console.log(record.get('name'));
});
// Parameterized query
const paramResult = await session.run(
'MATCH (p:Person) WHERE p.age >= $minAge RETURN p.name, p.age',
{ minAge: 25 }
);
// Explicit transaction
const tx = session.beginTransaction();
await tx.run('CREATE (n:Person {name: $name, age: $age})', { name: 'Alice', age: 30 });
await tx.run('CREATE (n:Person {name: $name, age: $age})', { name: 'Bob', age: 25 });
await tx.commit();
} finally {
await session.close();
await driver.close();
}
Go Example
package main
import (
"context"
"fmt"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)
func main() {
ctx := context.Background()
// Create driver (empty realm string for basic auth)
driver, err := neo4j.NewDriverWithContext(
"bolt://localhost:7687",
neo4j.BasicAuth("root", "playwithdata", ""),
)
if err != nil {
panic(err)
}
defer driver.Close(ctx)
session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "mydatabase"})
defer session.Close(ctx)
// Simple query
result, err := session.Run(ctx,
"MATCH (n:Person) RETURN n.name AS name LIMIT 10", nil)
if err != nil {
panic(err)
}
for result.Next(ctx) {
fmt.Println(result.Record().AsMap()["name"])
}
// Managed write transaction with automatic retry on transient errors
_, err = session.ExecuteWrite(ctx, func(tx neo4j.ManagedTransaction) (any, error) {
return tx.Run(ctx,
"CREATE (n:Person {name: $name, age: $age})",
map[string]any{"name": "Alice", "age": 30})
})
if err != nil {
panic(err)
}
}
Cypher Query Examples
Since BOLT protocol uses Cypher as its query language, you can execute any query supported by ArcadeDB’s OpenCypher implementation:
// Create vertices
CREATE (alice:Person {name: 'Alice', age: 30})
CREATE (bob:Person {name: 'Bob', age: 25})
// Create relationship
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
CREATE (a)-[:KNOWS {since: 2020}]->(b)
// Query with pattern matching
MATCH (p:Person)-[:KNOWS]->(friend)
WHERE p.age > 20
RETURN p.name, friend.name
// Variable-length paths
MATCH path = (start:Person)-[:KNOWS*1..3]->(end:Person)
RETURN path
// Aggregations
MATCH (p:Person)
RETURN avg(p.age) AS averageAge, count(p) AS totalPeople
Transactions
The BOLT protocol supports three transaction styles:
-
Auto-commit mode: single queries run outside a transaction are automatically committed.
-
Explicit transactions: use
BEGIN/COMMIT/ROLLBACKfor multi-statement transactions. Transactions are automatically rolled back if an error occurs. -
Managed transactions: the driver’s
executeRead/executeWritetransaction functions run your work in a retried transaction. If the server returns aNeo.TransientError.*(for example, a write-write conflict), the driver automatically retries the transaction function. Prefer this style for write paths that may contend.
// Auto-commit (implicit transaction)
session.run("CREATE (n:Person {name: 'Charlie'})");
// Explicit transaction with multiple operations
try (Transaction tx = session.beginTransaction()) {
tx.run("CREATE (a:Person {name: 'David'})");
tx.run("CREATE (b:Person {name: 'Eve'})");
tx.run("MATCH (a:Person {name: 'David'}), (b:Person {name: 'Eve'}) CREATE (a)-[:FRIENDS]->(b)");
tx.commit();
}
// Managed transaction — retried automatically on a transient error
session.executeWrite(tx -> {
tx.run("CREATE (n:Person {name: $name})", Values.parameters("name", "Grace"));
return null;
});
Causal Consistency (Bookmarks)
Bookmarks let a client guarantee it reads its own (or another session’s) writes. Capture the bookmarks from one session and pass them to the next; the server will not serve the follow-up read until it has caught up to that bookmark. This works across sessions and is exercised by the certification suite.
Bookmark bookmark;
try (Session write = driver.session(SessionConfig.forDatabase("mydatabase"))) {
write.run("CREATE (n:Person {name: 'Heidi'})").consume();
bookmark = write.lastBookmark();
}
// A later session reads with the bookmark to observe the write
try (Session read = driver.session(SessionConfig.builder()
.withDatabase("mydatabase")
.withBookmarks(bookmark)
.build())) {
read.run("MATCH (n:Person {name: 'Heidi'}) RETURN n").list();
}
Data Type Mapping
ArcadeDB emits native BOLT/PackStream structures for the following, so drivers receive typed objects (not strings):
| Category | Types |
|---|---|
Graph |
Node, Relationship, Path |
Temporal |
|
Other structured |
|
Values without a native BOLT type — RID and UUID — are serialized as strings. BigDecimal and out-of-range BigInteger values are converted to double and may lose precision.
|
Changed in v26.7.2 (breaking for Bolt clients). Temporal values ( |
Error Codes
Server errors are mapped to Neo4j-style structured status codes so driver-side error handling and retry logic behave as expected:
| Code | Raised when |
|---|---|
|
authentication failed or credentials were missing |
|
an operation is not permitted for the user |
|
the Cypher statement failed to parse |
|
the statement parsed but is semantically invalid |
|
a transaction reference is unknown |
|
a malformed or out-of-sequence protocol message |
|
a concurrent-modification / lock-timeout conflict — drivers retry managed transactions on this code |
|
an unexpected server-side failure |
Routing and High Availability
neo4j:// clients ask the server for a routing table. The response is HA-aware:
-
Single-node deployment: the node advertises itself for all roles (WRITE, READ, ROUTE).
-
HA cluster with a known leader: the leader is returned as WRITE + ROUTE and the followers as READ + ROUTE, so driver-side load balancing works.
-
HA cluster mid-election (leader unknown): the node is advertised as READ + ROUTE only, never WRITE, until a leader is elected.
The routing table’s time-to-live is controlled by arcadedb.bolt.routing.ttl (default 300 s).
|
For clusters where nodes expose the BOLT port on different ports, each node’s client-reachable BOLT address must be declared using the object form of |
Current Limitations
-
Authentication: the
nonescheme is not accepted — ArcadeDB always requires credentials. This is intentional, not a bug. -
Type fidelity:
RIDandUUIDare returned as strings;BigDecimal/ oversizedBigIntegerare down-converted todouble. -
Heterogeneous cluster ports: see the note under Routing and High Availability.
Troubleshooting
If you encounter connection issues:
-
Enable debug logging: Start the server with
-Darcadedb.bolt.debug=trueto see detailed protocol messages. -
Check port availability: Ensure port 7687 (or your configured port) is not in use by another service.
-
Verify authentication: ArcadeDB always requires authentication. Ensure you’re providing valid credentials — the
nonescheme is rejected. -
Match the encryption mode: If the server runs with
arcadedb.bolt.ssl=REQUIRED, connect withbolt+s:///neo4j+s://. If TLS isDISABLED(the default), connect withbolt:///neo4j://and encryption turned off in the driver.
To connect without encryption against a plaintext server:
// Java
Config.builder().withoutEncryption().build()
# Python
driver = GraphDatabase.driver(uri, auth=auth, encrypted=False)