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 spatial Point values

  • 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

arcadedb.bolt.port

7687

TCP/IP port for BOLT connections

arcadedb.bolt.host

0.0.0.0

Host/IP address to bind to

arcadedb.bolt.defaultDatabase

(none)

Default database when not specified by the client; if unset, the first available database is used

arcadedb.bolt.maxConnections

0

Maximum concurrent connections (0 = unlimited)

arcadedb.bolt.ssl

DISABLED

TLS mode for BOLT connections: DISABLED, OPTIONAL, or REQUIRED (see TLS/SSL)

arcadedb.bolt.routing.ttl

300

Time-to-live in seconds for routing table entries returned to neo4j:// clients

arcadedb.bolt.debug

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

DISABLED

(default) plaintext only. Clients connect with bolt:// / neo4j:// and encryption off.

OPTIONAL

the server auto-detects the connection: a TLS handshake is served encrypted, a plaintext connection is served in the clear. Useful during migration.

REQUIRED

only encrypted connections are accepted. Clients must use bolt+s:// / neo4j+s://.

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/SUCCESS response the server reports itself as Neo4j/5.26.0 compatible (ArcadeDB <version>).

  • CALL dbms.components() returns Neo4j Kernel, version 5.26.0, edition community.

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, spatial Point, byte arrays, and nested lists/maps.

  • Structured Neo.ClientError. and Neo.TransientError. error codes.

  • HA-aware neo4j:// routing.

Explicit, documented differences from a real Neo4j server (never silent omissions):

  • The none authentication scheme is intentionally rejected — ArcadeDB always requires credentials.

  • RID and UUID values are serialized as strings (there is no native BOLT type for them).

  • BigDecimal and out-of-range BigInteger values are down-converted to double, which may lose precision.

  • Aura and enterprise-clustering-specific driver behaviors are out of scope.

  • The advertised server_agent version (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

neo4j-java-driver

Maven: org.neo4j.driver:neo4j-java-driver

Python

neo4j

pip install neo4j

JavaScript

neo4j-driver

npm install neo4j-driver

.NET

Neo4j.Driver

NuGet: Neo4j.Driver

Go

neo4j-go-driver

go get github.com/neo4j/neo4j-go-driver/v5

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

bolt://

Direct connection to a single server, plaintext.

bolt+s://

Direct connection to a single server, TLS-encrypted.

neo4j://

Routing/discovery connection — the driver asks the server for the cluster routing table (HA-aware).

neo4j+s://

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/ROLLBACK for multi-statement transactions. Transactions are automatically rolled back if an error occurs.

  • Managed transactions: the driver’s executeRead/executeWrite transaction functions run your work in a retried transaction. If the server returns a Neo.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

Date, Time, LocalTime, DateTime, LocalDateTime (DateTime uses the UTC encoding on BOLT 5.0+, legacy encoding on 4.x)

Other structured

Duration, spatial Point (2D/3D), byte arrays, nested lists and maps, null

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 (Date, Time, LocalTime, DateTime, LocalDateTime) are now carried as native PackStream temporal structures in both directions. Before v26.7.2 they were sent as ISO-8601 strings, and inbound datetime query parameters were silently dropped. A client that previously read a temporal property as a String must now read the native temporal type — e.g. Value.asZonedDateTime() or Value.asLocalDate() — exactly as it would against Neo4j. See Upgrade — Breaking changes.

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

Neo.ClientError.Security.Unauthorized

authentication failed or credentials were missing

Neo.ClientError.Security.Forbidden

an operation is not permitted for the user

Neo.ClientError.Statement.SyntaxError

the Cypher statement failed to parse

Neo.ClientError.Statement.SemanticError

the statement parsed but is semantically invalid

Neo.ClientError.Transaction.TransactionNotFound

a transaction reference is unknown

Neo.ClientError.Request.Invalid

a malformed or out-of-sequence protocol message

Neo.TransientError.Transaction.DeadlockDetected

a concurrent-modification / lock-timeout conflict — drivers retry managed transactions on this code

Neo.DatabaseError.General.UnknownError

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 arcadedb.ha.serverList (host:{raft:..,http:..,bolt:..}). When omitted, each peer’s BOLT address is derived from its Raft host plus this node’s BOLT port, which is correct only for homogeneous deployments (for example, a Kubernetes StatefulSet).

Current Limitations

  • Authentication: the none scheme is not accepted — ArcadeDB always requires credentials. This is intentional, not a bug.

  • Type fidelity: RID and UUID are returned as strings; BigDecimal / oversized BigInteger are down-converted to double.

  • Heterogeneous cluster ports: see the note under Routing and High Availability.

Troubleshooting

If you encounter connection issues:

  1. Enable debug logging: Start the server with -Darcadedb.bolt.debug=true to see detailed protocol messages.

  2. Check port availability: Ensure port 7687 (or your configured port) is not in use by another service.

  3. Verify authentication: ArcadeDB always requires authentication. Ensure you’re providing valid credentials — the none scheme is rejected.

  4. Match the encryption mode: If the server runs with arcadedb.bolt.ssl=REQUIRED, connect with bolt+s:// / neo4j+s://. If TLS is DISABLED (the default), connect with bolt:// / 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)