bolt

package
v1.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 35 Imported by: 0

README

NornicDB Bolt Protocol Server

Neo4j-compatible Bolt protocol server for NornicDB. Enables any Neo4j driver to connect to NornicDB without modifications.

✅ Implementation Status

Phase 1: Bolt Protocol Server - COMPLETE

  • ✅ TCP Server & Protocol Handler
  • ✅ PackStream Serialization (encoding & decoding)
  • ✅ Message Handling (HELLO, RUN, PULL, DISCARD, BEGIN, COMMIT, ROLLBACK, RESET, GOODBYE)
  • ✅ Authentication Handshake
  • ✅ Session Management
  • ✅ Result Streaming
  • ✅ Comprehensive Unit Tests (2200+ lines)
  • ✅ Integration Tests with Cypher Executor
  • ✅ Stress Testing

Features

Protocol Support
  • Bolt 4.x: Full support for Bolt 4.0, 4.1, 4.2, 4.3, 4.4
  • PackStream: Complete binary serialization format
  • Streaming: Efficient result streaming with PULL/DISCARD
  • Transactions: BEGIN, COMMIT, ROLLBACK support
  • Connection Pooling: Multiple concurrent connections
WebSocket transport

The Bolt port multiplexes four wire-level transports based on the first 5 bytes of every accepted connection — mirroring Neo4j's TransportSelectionHandler exactly:

First bytes Wire-level transport Metric label
Bolt magic 60 60 B0 17 raw TCP tcp
TLS handshake (0x16), then Bolt magic TLS + raw tcp_tls
GET (HTTP/1.1 upgrade) WebSocket ws
TLS handshake, then GET TLS + WebSocket ws_tls

How clients reach each branch:

  • Official Neo4j drivers dial bolt:// / bolt+s:// (or the neo4j:// routing wrappers). The Node / JVM / Python builds produce raw TCP; the JS browser build produces a GET WebSocket upgrade from the same bolt:// URL.
  • Third-party tools speaking raw WebSockets dial ws:// / wss:// directly and write Bolt frames into BinaryMessage payloads — same wire bytes the JS browser build produces internally.

A plain GET / (no Upgrade headers) returns a 200 OK discovery response — empty body when OAuth is not configured (Community parity), JSON describing the OAuth provider when it is.

WebSocket sessions speak the same Bolt wire format inside binary frames: same magic, same version negotiation, same chunked message framing.

TLS

Cert+key paths are read on every TLS handshake (the tls.Config.GetCertificate callback) and a 5-second background ticker re-loads them from disk. Operator update protocol: write to cert.pem.new then mv cert.pem.new cert.pem.

RequireTLS=true rejects every plaintext connection (raw or WS) with the canonical Neo4j error message. mTLS is opt-in via BoltTLSClientCAFile + a BoltTLSClientAuthMode enum (none / request / request_verify / require_verify).

Message Types
Message Type Status Description
HELLO 0x01 Authentication handshake
GOODBYE 0x02 Clean disconnect
RESET 0x0F Reset session state
RUN 0x10 Execute Cypher query
DISCARD 0x2F Discard remaining results
PULL 0x3F Stream result records
BEGIN 0x11 Start transaction
COMMIT 0x12 Commit transaction
ROLLBACK 0x13 Rollback transaction
ROUTE 0x66 Cluster routing (no-op)
Response Messages
Message Type Status Description
SUCCESS 0x70 Operation succeeded
RECORD 0x71 Result row
IGNORED 0x7E Request ignored
FAILURE 0x7F Operation failed

Usage

Starting the Server
Option 1: Command Line
# Build the server
cd cmd/nornicdb-bolt
go build

# Start with defaults (port 7687)
./nornicdb-bolt

# Start on custom port
./nornicdb-bolt -port 7688

# Custom data directory
./nornicdb-bolt -data ./mydata
Option 2: Programmatic
package main

import (
    "context"
    "github.com/orneryd/nornicdb/pkg/bolt"
    "github.com/orneryd/nornicdb/pkg/cypher"
    "github.com/orneryd/nornicdb/pkg/storage"
)

func main() {
    // Create storage
    store := storage.NewMemoryEngine()

    // Create Cypher executor
    cypherExec := cypher.NewStorageExecutor(store)

    // Wrap for Bolt
    executor := &MyBoltExecutor{cypher: cypherExec}

    // Configure server
    config := &bolt.Config{
        Port:            7687,
        MaxConnections:  100,
        ReadBufferSize:  8192,
        WriteBufferSize: 8192,
    }

    // Start server
    server := bolt.New(config, executor)
    if err := server.ListenAndServe(); err != nil {
        panic(err)
    }
}

// MyBoltExecutor implements bolt.QueryExecutor
type MyBoltExecutor struct {
    cypher *cypher.StorageExecutor
}

func (m *MyBoltExecutor) Execute(ctx context.Context, query string, params map[string]any) (*bolt.QueryResult, error) {
    result, err := m.cypher.Execute(ctx, query, params)
    if err != nil {
        return nil, err
    }
    return &bolt.QueryResult{
        Columns: result.Columns,
        Rows:    result.Rows,
    }, nil
}
Connecting with Neo4j Drivers
Python
from neo4j import GraphDatabase

# Connect to NornicDB
driver = GraphDatabase.driver("bolt://localhost:7687")

with driver.session() as session:
    # Create a node
    result = session.run(
        "CREATE (n:Person {name: $name, age: $age}) RETURN n",
        name="Alice",
        age=30
    )
    print(result.single()[0])

    # Query nodes
    result = session.run("MATCH (n:Person) RETURN n.name, n.age")
    for record in result:
        print(f"{record['n.name']}: {record['n.age']}")

driver.close()
JavaScript/TypeScript
const neo4j = require("neo4j-driver");

// Connect to NornicDB
const driver = neo4j.driver(
  "bolt://localhost:7687",
  neo4j.auth.basic("", ""), // Auth not required yet
);

const session = driver.session();

try {
  // Create a node
  const result = await session.run(
    "CREATE (n:Person {name: $name, age: $age}) RETURN n",
    { name: "Bob", age: 25 },
  );
  console.log(result.records[0].get("n"));

  // Query nodes
  const queryResult = await session.run("MATCH (n:Person) RETURN n");
  queryResult.records.forEach((record) => {
    console.log(record.get("n"));
  });
} finally {
  await session.close();
}

await driver.close();
Go
package main

import (
    "context"
    "fmt"
    "github.com/neo4j/neo4j-go-driver/v5/neo4j"
)

func main() {
    // Connect to NornicDB
    driver, err := neo4j.NewDriverWithContext(
        "bolt://localhost:7687",
        neo4j.NoAuth(),
    )
    if err != nil {
        panic(err)
    }
    defer driver.Close(context.Background())

    ctx := context.Background()
    session := driver.NewSession(ctx, neo4j.SessionConfig{})
    defer session.Close(ctx)

    // Create a node
    result, err := session.Run(ctx,
        "CREATE (n:Person {name: $name, age: $age}) RETURN n",
        map[string]any{"name": "Charlie", "age": 28},
    )
    if err != nil {
        panic(err)
    }

    if result.Next(ctx) {
        node := result.Record().Values[0]
        fmt.Printf("Created: %v\n", node)
    }

    // Query nodes
    result, _ = session.Run(ctx, "MATCH (n:Person) RETURN n", nil)
    for result.Next(ctx) {
        fmt.Println(result.Record().Values[0])
    }
}
Java
import org.neo4j.driver.*;

public class NornicDBExample {
    public static void main(String[] args) {
        // Connect to NornicDB
        Driver driver = GraphDatabase.driver(
            "bolt://localhost:7687",
            AuthTokens.none()
        );

        try (Session session = driver.session()) {
            // Create a node
            Result result = session.run(
                "CREATE (n:Person {name: $name, age: $age}) RETURN n",
                Values.parameters("name", "David", "age", 35)
            );
            System.out.println(result.single().get("n"));

            // Query nodes
            result = session.run("MATCH (n:Person) RETURN n");
            while (result.hasNext()) {
                System.out.println(result.next().get("n"));
            }
        }

        driver.close();
    }
}
Transaction Support

This example is atomic when the Bolt server is configured through NewWithDatabaseManager or a SessionExecutorFactory that returns a distinct TransactionalExecutor for each connection. A directly supplied TransactionalExecutor is supported only when MaxConnections is exactly one; cleanup failure or uncertain commit failure quarantines it against reuse. Multi-connection servers reject BEGIN for a shared raw executor. A plain QueryExecutor acknowledges transaction-control messages for protocol compatibility, but each RUN remains auto-committed and cannot be rolled back.

from neo4j import GraphDatabase

driver = GraphDatabase.driver("bolt://localhost:7687")

with driver.session() as session:
    # Explicit transaction
    tx = session.begin_transaction()

    try:
        tx.run("CREATE (n:Person {name: 'Eve'})")
        tx.run("CREATE (n:Person {name: 'Frank'})")
        tx.commit()
        print("Transaction committed")
    except Exception as e:
        tx.rollback()
        print(f"Transaction rolled back: {e}")

Architecture

┌─────────────────────────────────┐
│   Neo4j Driver (Any Language)   │
│  Python, JS, Go, Java, .NET...  │
└───────────────┬─────────────────┘
                │
                │ Bolt Protocol (TCP)
                │ PackStream Format
                │
┌───────────────▼─────────────────┐
│      Bolt Protocol Server        │
│  • Handshake & Authentication    │
│  • Session Management            │
│  • Message Routing               │
│  • Result Streaming              │
└───────────────┬─────────────────┘
                │
                │ QueryExecutor Interface
                │
┌───────────────▼─────────────────┐
│      Cypher Executor             │
│  • Query Parsing                 │
│  • Execution Planning            │
│  • Parameter Substitution        │
└───────────────┬─────────────────┘
                │
┌───────────────▼─────────────────┐
│      Storage Engine              │
│  • MemoryEngine (in-memory)      │
│  • BadgerEngine (persistent)     │
└─────────────────────────────────┘

Testing

Run Unit Tests
cd pkg/bolt
go test -v
Run Integration Tests
go test -v -run TestBoltCypherIntegration
Run Stress Tests
go test -v -run TestBoltServerStress
Test with Real Driver
# Terminal 1: Start server
cd cmd/nornicdb-bolt
go run main.go

# Terminal 2: Run Python test
pip install neo4j-driver
python3 << EOF
from neo4j import GraphDatabase
driver = GraphDatabase.driver("bolt://localhost:7687")
with driver.session() as session:
    result = session.run("CREATE (n:Test {id: 1}) RETURN n")
    print("Success:", result.single()[0])
driver.close()
EOF

Performance

Benchmarks
Operation Neo4j NornicDB Speedup
Connection ~2ms ~1ms 2x
Simple Query ~1ms ~0.5ms 2x
Create Node ~2ms ~0.8ms 2.5x
Match Query ~1.5ms ~0.6ms 2.5x
Vector Search ~10ms ~3ms 3.3x
Bulk Insert (1K) ~100ms ~40ms 2.5x

Why faster?

  • In-memory storage (no disk I/O)
  • Native Go implementation (no JVM overhead)
  • Optimized PackStream encoding
  • Efficient connection pooling
Scalability
  • Concurrent connections: 100+ default, configurable up to 1000+
  • Throughput: ~10K queries/sec on commodity hardware
  • Memory: ~50MB base + ~1KB per connection
  • Latency: P50: 0.5ms, P95: 2ms, P99: 5ms

Protocol Details

Handshake Flow
Client                              Server
  │                                   │
  ├─ Magic: 0x6060B017 ─────────────►│
  ├─ Versions: [4.4, 4.3, 4.2, 4.1] ─►│
  │                                   │
  │◄─────────── Selected: 4.4 ────────┤
  │                                   │
  ├─ HELLO {user_agent: ...} ────────►│
  │                                   │
  │◄─ SUCCESS {server: "NornicDB"} ───┤
Query Execution Flow
Client                              Server
  │                                   │
  ├─ RUN {query, params} ───────────►│
  │                                   │ Execute Query
  │◄─ SUCCESS {fields: [...]} ────────┤
  │                                   │
  ├─ PULL {n: 100} ─────────────────►│
  │                                   │ Stream Results
  │◄─ RECORD [row1] ──────────────────┤
  │◄─ RECORD [row2] ──────────────────┤
  │◄─ RECORD [row3] ──────────────────┤
  │◄─ SUCCESS {has_more: false} ──────┤
Transaction Flow
Client                              Server
  │                                   │
  ├─ BEGIN ─────────────────────────►│
  │◄─ SUCCESS ────────────────────────┤
  │                                   │
  ├─ RUN {query1} ──────────────────►│
  │◄─ SUCCESS ────────────────────────┤
  │                                   │
  ├─ RUN {query2} ──────────────────►│
  │◄─ SUCCESS ────────────────────────┤
  │                                   │
  ├─ COMMIT ─────────────────────────►│
  │◄─ SUCCESS ────────────────────────┤

BEGIN may include tx_timeout, a signed PackStream long measured in milliseconds, or null. Matching Neo4j 5.26, missing, null, zero, and negative values disable the client deadline; positive values beyond Go's duration range are accepted and saturated. The lifetime clock starts when a validated BEGIN is admitted to backend allocation, so allocation time, idle time, and active RUN work all count. If a backend accepts BEGIN after the deadline, BEGIN still succeeds, then NornicDB immediately owns rollback and the next transaction operation reports the timeout. Expiry cancels an active query, rolls the storage transaction back, and makes a later COMMIT fail with Neo4j's Neo.ClientError.Transaction.TransactionTimedOutClientConfiguration status. An absent, null, zero, or negative timeout does not schedule expiry.

Every explicit transaction has one terminal owner. COMMIT stops a pending deadline before entering storage; timeout, ROLLBACK, RESET, GOODBYE, and connection loss otherwise use the same exactly-once rollback path. Cleanup uses a five-second request context independent of connection cancellation. The session lifecycle owns operation admission for every supported per-session TransactionalExecutor. If a timeout cancels an active RUN or expires during a deferred explicit-transaction result flush, that operation performs the pending Badger/WAL rollback before the session responds or processes another message. RESET and connection teardown wait for that completion handoff. An admitted backend that does not honor the request context remains synchronously owned rather than being abandoned in a leaked cleanup goroutine, so the five seconds is cooperative rather than a hard wall-clock bound for such an implementation. If rollback errors or panics, or a commit returns an uncertain error, NornicDB does not claim that storage was released: it closes the connection, suppresses deferred flush, and quarantines a directly supplied single-connection executor. Timeout responses wait for owned cleanup; cleanup failure closes the connection instead of exposing a reusable timeout state. A deferred PULL/DISCARD flush error marks the explicit transaction failed until RESET rolls it back. Explicit terminal paths discard unconsumed result state, and CommitTransaction alone owns successful commit durability—rolled-back or committed writes are never sent through a later legacy flush. Idle expiry performs rollback before invoking its completion diagnostic; the separate cleanup-request diagnostic is emitted only for an active executor operation handoff, so an unresponsive logging callback cannot delay idle cleanup.

Compatibility

Supported Drivers
Driver Language Version Status
neo4j-python-driver Python 5.x ✅ Tested
neo4j-javascript-driver JavaScript/TS 5.x ✅ Tested
neo4j-go-driver Go 5.x ✅ Tested
Neo4j.Driver .NET/C# 5.x ⏳ Should work
neo4j-java-driver Java 5.x ⏳ Should work
neo4j-ruby-driver Ruby 5.x ⏳ Should work
rustheus Rust Latest ⏳ Should work
Known Limitations
  1. No User Authentication: Currently accepts all connections (Phase 2)
  2. No Cluster Routing: Single-node only (future enhancement)
  3. No Streaming Large Results: Buffered in memory (optimization needed)

Roadmap

Completed ✅
  • Bolt 4.x protocol implementation
  • PackStream serialization
  • Message handling (all types)
  • Session management
  • Result streaming
  • Unit tests (2200+ lines)
  • Integration tests
  • Stress tests
  • Command-line server
  • Atomic explicit transactions with timeout and disconnect cleanup through the database-manager or per-connection TransactionalExecutor path
In Progress 🔄
  • Schema management (constraints, indexes) - See Phase 2
  • Built-in procedures (vector, fulltext, apoc) - See Phase 3
Planned 📋
  • User authentication and RBAC
  • TLS/SSL support
  • Connection pooling optimizations
  • Large result streaming (chunked)
  • Query result caching
  • Performance monitoring
  • Cluster mode support

Troubleshooting

Connection Refused
# Check if server is running
lsof -i :7687

# Start server if not running
cd cmd/nornicdb-bolt
go run main.go
Driver Compatibility Issues
# Use latest driver version
pip install --upgrade neo4j-driver

# Verify connection
from neo4j import GraphDatabase
driver = GraphDatabase.driver("bolt://localhost:7687")
driver.verify_connectivity()
cypher-shell compatibility override

cypher-shell may reject a Bolt connection if the Bolt HELLO success metadata does not advertise a Neo4j server string. If that is the only blocker, opt into the announcement override:

export NORNICDB_BOLT_SERVER_ANNOUNCEMENT="Neo4j/5.26.0"
./nornicdb serve
cypher-shell -a bolt://localhost:7687 -u neo4j -p password

This changes only the announced Bolt server string. Leave it unset unless you need strict-client compatibility.

Performance Issues
// Increase connection pool size
config := &bolt.Config{
    Port:           7687,
    MaxConnections: 500,  // Increase from 100
}
Memory Issues
# Monitor memory usage
ps aux | grep nornicdb-bolt

# Reduce max connections if needed
./nornicdb-bolt -maxconn 50

Contributing

See IMPLEMENTATION_PLAN.md for the full development roadmap.

Running Tests
# All tests
go test ./pkg/bolt/...

# Verbose
go test -v ./pkg/bolt/...

# With coverage
go test -cover ./pkg/bolt/...

# Specific test
go test -run TestBoltCypherIntegration ./pkg/bolt/...

License

MIT License - See LICENSE for details.


Status: ✅ Phase 1 Complete - Ready for Phase 2 (Schema Management)
Last Updated: November 25, 2025
Version: 1.0.0

Documentation

Overview

Package bolt provides authentication adapters for integrating with NornicDB's auth package.

Package bolt — Plan 04-02 metric instrumentation.

Three observation sites per CONTEXT D-11/a/c:

  1. Connection accept (handleConnection in server.go): Inc/Dec ConnectionsActive gauge; observe SessionDuration on close; increment ConnectionsTotal{result} on close.
  2. Per-message dispatch loop (handleMessage / dispatchMessage in server.go): observe MessageDuration{op} per message; increment MessagesTotal{op, result}.
  3. Packstream decode boundary (packstream.go): increment PackstreamDecodeErrors{reason} via reasonFromError() classifier (closed enum: truncated, invalid_marker, wrong_type, oversize).

PULL chunks NOT separately observed (D-11b — chunk timing rolls up into parent PULL message_duration_seconds; matches Phase 8 TRC-13).

Auth crosswire (CONTEXT D-11 / D-05e + Plan 04-06 forward-compat): HELLO completion increments auth_attempts_total{result, protocol="bolt"} when authMetrics is non-nil. Plan 04-06 owns the AuthMetrics bag and wires it via SetAuthMetrics(...); this plan adds the call site behind a nil-check that no-ops until 04-06 ships.

Hot-path discipline (MET-25): per-op BoundLatencyObserver pre-built at SetBoltMetrics time and indexed by op name in a small map; the dispatch loop pays a single map lookup per message — no WithLabelValues alloc.

Package bolt implements the Neo4j Bolt protocol server for NornicDB.

Package bolt — Plan 04-02 Task 04-02-05 packstream decode-error reason classification (CONTEXT D-11c).

Goal: every packstream decode failure reaches nornicdb_bolt_packstream_decode_errors_total{reason} under exactly ONE of the closed enum values defined in observability.AllowedPackstreamReasons:

truncated      — incomplete data / out of bounds / EOF mid-decode
invalid_marker — unknown marker byte / not-a-X type-tag mismatch
wrong_type     — decoded value is the wrong shape for its consumer
oversize       — declared length exceeds the buffer / size limit

Free-form err.Error() strings MUST NEVER reach the *Vec — that would be a cardinality bomb (RESEARCH §Q11; Phase 3 D-03a / D-04 belt). The classifier is the chokepoint: callers pass the error through reasonFromError(err) which emits a closed-enum string OR returns "" to signal "non-decode error, do not observe".

Sentinel errors (errTruncated, errInvalidMarker, errWrongType, errOversize) provide a typed seam — packstream.go can wrap a fmt.Errorf with errors.Is-friendly sentinels at known sites, and reasonFromError(err) uses errors.Is first for fast/correct matches, falling back to substring detection on the legacy fmt.Errorf messages that already exist in the decoder.

Package bolt implements the Neo4j Bolt protocol server for NornicDB.

This package provides a Bolt protocol server that allows existing Neo4j drivers and tools to connect to NornicDB without modification. The server implements Bolt 4.x protocol specifications for maximum compatibility.

Neo4j Bolt Protocol Compatibility:

  • Bolt 4.0, 4.1, 4.2, 4.3, 4.4 support
  • PackStream serialization format
  • Transaction management (BEGIN, COMMIT, ROLLBACK)
  • Streaming result sets (RUN, PULL, DISCARD)
  • Authentication handshake
  • Connection pooling support

Supported Neo4j Drivers:

  • Neo4j Java Driver
  • Neo4j Python Driver (neo4j-driver)
  • Neo4j JavaScript Driver
  • Neo4j .NET Driver
  • Neo4j Go Driver
  • Community drivers (Rust, Ruby, etc.)

Example Usage:

// Create Bolt server with Cypher executor
config := bolt.DefaultConfig()
config.Port = 7687
config.MaxConnections = 100

// Implement query executor
executor := &MyQueryExecutor{db: nornicDB}

server := bolt.New(config, executor)

// Start server
if err := server.ListenAndServe(); err != nil {
	log.Fatal(err)
}

// Server is now accepting Bolt connections on port 7687

Client Usage (any Neo4j driver):

// Python example
from neo4j import GraphDatabase

driver = GraphDatabase.driver("bolt://localhost:7687")
with driver.session() as session:
    result = session.run("MATCH (n) RETURN count(n)")
    print(result.single()[0])

// Go example
driver, _ := neo4j.NewDriver("bolt://localhost:7687", neo4j.NoAuth())
session := driver.NewSession(neo4j.SessionConfig{})
result, _ := session.Run("MATCH (n) RETURN count(n)", nil)

Protocol Flow:

1. **Handshake**:

  • Client sends magic number (0x6060B017)
  • Client sends supported versions
  • Server responds with selected version

2. **Authentication**:

  • Client sends HELLO message with credentials
  • Server responds with SUCCESS or FAILURE

3. **Query Execution**:

  • Client sends RUN message with Cypher query
  • Server responds with SUCCESS (field names)
  • Client sends PULL to stream results
  • Server sends RECORD messages + final SUCCESS

4. **Transaction Management**:

  • BEGIN: Start explicit transaction
  • COMMIT: Commit transaction
  • ROLLBACK: Rollback transaction

Message Types:

  • HELLO: Authentication
  • RUN: Execute Cypher query
  • PULL: Stream result records
  • DISCARD: Discard remaining results
  • BEGIN/COMMIT/ROLLBACK: Transaction control
  • RESET: Reset session state
  • GOODBYE: Close connection

PackStream Encoding:

The Bolt protocol uses PackStream for efficient binary serialization:
- Compact representation of common types
- Support for nested structures
- Streaming-friendly format

Performance:

  • Binary protocol (faster than HTTP/JSON)
  • Connection pooling and reuse
  • Streaming results (low memory usage)
  • Pipelining support

ELI12 (Explain Like I'm 12):

Think of the Bolt server like a translator at the United Nations:

  1. **Different languages**: Neo4j drivers speak "Bolt language" but NornicDB speaks "NornicDB language". The Bolt server translates between them.

  2. **Same conversation**: The drivers can have the same conversation they always had (asking questions in Cypher), they just don't know they're talking to a different database!

  3. **Binary messages**: Instead of sending text messages (like HTTP), Bolt sends compact binary messages - like sending a compressed file instead of a text document. Much faster!

  4. **Streaming**: Instead of waiting for ALL results before sending anything, Bolt can send results one-by-one as they're found, like a live news feed.

This lets existing Neo4j tools work with NornicDB without any changes!

Package bolt: TLS construction helpers.

Mirrors Neo4j's sslContext + requiresEncryption pattern; cert rotation requires atomic rename of cert+key files. The returned *tls.Config leaves Certificates nil and installs a GetCertificate closure that re-loads from disk on every handshake. A 5s background ticker periodically re-reads the files; transient failures during the periodic reload are absorbed (the previous cert stays cached) — operator update protocol is atomic rename so partial reads are an expected event, not a logged anomaly.

Index

Constants

View Source
const (
	BoltV4_4 = 0x0404 // Bolt 4.4
	BoltV4_3 = 0x0403 // Bolt 4.3
	BoltV4_2 = 0x0402 // Bolt 4.2
	BoltV4_1 = 0x0401 // Bolt 4.1
	BoltV4_0 = 0x0400 // Bolt 4.0
)

Protocol versions supported

View Source
const (
	MsgHello    byte = 0x01
	MsgGoodbye  byte = 0x02
	MsgReset    byte = 0x0F
	MsgRun      byte = 0x10
	MsgDiscard  byte = 0x2F
	MsgPull     byte = 0x3F
	MsgBegin    byte = 0x11
	MsgCommit   byte = 0x12
	MsgRollback byte = 0x13
	MsgRoute    byte = 0x66

	// Response messages
	MsgSuccess byte = 0x70
	MsgRecord  byte = 0x71
	MsgIgnored byte = 0x7E
	MsgFailure byte = 0x7F
)

Message types

Variables

View Source
var AllowedBoltTransports = observability.AllowedBoltTransports

AllowedBoltTransports re-exports the closed enum from pkg/observability so call sites in this package can refer to it without importing the observability package directly.

View Source
var ErrUnencryptedRequired = errors.New("An unencrypted connection attempt was made where encryption is required.")

ErrUnencryptedRequired is returned when peekTransport refuses a plaintext connection because RequireTLS is set. The error text matches Neo4j's canonical message verbatim so existing driver assertions pass unchanged.

Functions

func LoadTLSConfig added in v1.1.2

func LoadTLSConfig(certFile, keyFile string) (*tls.Config, error)

LoadTLSConfig loads cert+key from disk and returns a *tls.Config suitable for the Bolt listener. The returned config has MinVersion=TLS1.2 and a GetCertificate closure that re-reads the cert+key from disk on every handshake (cert rotation). Certificates is intentionally left nil so the closure fires on every handshake. A 5s background ticker re-reads the files in a goroutine and swaps the cached cert under a sync.RWMutex on successful load. Failures during the periodic reload are logged but do not invalidate the previous cert.

func LoadTLSConfigWithClientCA added in v1.1.2

func LoadTLSConfigWithClientCA(certFile, keyFile, clientCAFile string, mode ClientAuthMode) (*tls.Config, error)

LoadTLSConfigWithClientCA additionally verifies client certs against clientCAFile (for mTLS). mode controls tls.Config.ClientAuth. When clientCAFile is "" and mode is ClientAuthNone, behaves identically to LoadTLSConfig.

Types

type AuthenticatorAdapter

type AuthenticatorAdapter struct {
	// contains filtered or unexported fields
}

AuthenticatorAdapter wraps auth.Authenticator to implement BoltAuthenticator. This allows the Bolt server to use the same authentication system as the HTTP server, service accounts, and the UI.

The adapter translates Neo4j-style Bolt authentication (scheme, principal, credentials) to NornicDB's auth.Authenticator (username, password or JWT token).

Supported authentication schemes:

  • "basic": Username/password authentication (same as HTTP basic auth)
  • "bearer": JWT token authentication (for cluster inter-node auth)
  • "none": Anonymous access (if enabled, grants viewer role)

Cluster Authentication with Shared JWT

For cluster deployments where all nodes need to authenticate with each other, use bearer token authentication with a shared JWT secret:

  1. Configure all nodes with the same JWT secret: NORNICDB_JWT_SECRET=your-shared-secret-min-32-bytes

  2. Generate a cluster token on any node: POST /api/v1/auth/cluster-token {"node_id": "node-2", "role": "admin"}

  3. Connect from other nodes using the bearer scheme: driver = GraphDatabase.driver("bolt://node1:7687", auth=("", token)) # Empty username triggers bearer auth

Example:

// Create the shared authenticator
authConfig := auth.DefaultAuthConfig()
authConfig.JWTSecret = []byte("your-secret-key-shared-across-cluster")
authenticator, _ := auth.NewAuthenticator(authConfig)

// Create service accounts for server-to-server communication
authenticator.CreateUser("cluster-node-1", "secure-password", []auth.Role{auth.RoleAdmin})
authenticator.CreateUser("backup-service", "backup-password", []auth.Role{auth.RoleViewer})

// Create Bolt server with shared auth
boltConfig := bolt.DefaultConfig()
boltConfig.Authenticator = bolt.NewAuthenticatorAdapter(authenticator)
boltConfig.RequireAuth = true

boltServer := bolt.New(boltConfig, executor)

func NewAuthenticatorAdapter

func NewAuthenticatorAdapter(authenticator *auth.Authenticator) *AuthenticatorAdapter

NewAuthenticatorAdapter creates a new BoltAuthenticator that wraps auth.Authenticator. This enables the Bolt server to use the same user database and authentication as the HTTP server, ensuring consistent auth across all protocols.

Parameters:

  • authenticator: The shared auth.Authenticator instance

Example:

authenticator, _ := auth.NewAuthenticator(auth.DefaultAuthConfig())
boltAuth := bolt.NewAuthenticatorAdapter(authenticator)

config := bolt.DefaultConfig()
config.Authenticator = boltAuth
config.RequireAuth = true

func NewAuthenticatorAdapterWithAnonymous

func NewAuthenticatorAdapterWithAnonymous(authenticator *auth.Authenticator) *AuthenticatorAdapter

NewAuthenticatorAdapterWithAnonymous creates an adapter that allows anonymous connections. Anonymous users receive "viewer" role (read-only access).

Use with caution - this allows unauthenticated connections.

func (*AuthenticatorAdapter) Authenticate

func (a *AuthenticatorAdapter) Authenticate(scheme, principal, credentials string) (*BoltAuthResult, error)

Authenticate validates credentials from the Bolt HELLO message. This method implements the BoltAuthenticator interface.

Supported schemes:

  • "basic": Username/password authentication (same as HTTP basic auth)
  • "bearer": JWT token authentication (credentials contains JWT, principal is ignored)
  • "none": Anonymous access (if enabled, grants viewer role)

Cluster Authentication

For server-to-server clustering, you have two options:

Option 1: Service accounts with "basic" scheme

authenticator.CreateUser("cluster-node-west", "secure-password-123",
	[]auth.Role{auth.RoleAdmin})
driver = GraphDatabase.driver("bolt://node-east:7687",
	basic_auth("cluster-node-west", "secure-password-123"))

Option 2: JWT tokens with "bearer" scheme (recommended for clusters)

# Generate token via API:
curl -X POST http://node:7474/api/v1/auth/cluster-token \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"node_id": "node-2", "role": "admin"}'

# Connect with bearer token:
driver = GraphDatabase.driver("bolt://node:7687",
	basic_auth("", token))  # Empty username = bearer auth

func (*AuthenticatorAdapter) SetAllowAnonymous

func (a *AuthenticatorAdapter) SetAllowAnonymous(allow bool)

SetAllowAnonymous enables or disables anonymous authentication.

func (*AuthenticatorAdapter) SetGetEffectivePermissions

func (a *AuthenticatorAdapter) SetGetEffectivePermissions(fn func(roles []string) []string)

SetGetEffectivePermissions sets the callback used to resolve roles to effective permission IDs. When set, BoltAuthResult.Permissions is filled so HasPermission uses stored role entitlements.

type BoltAuthResult

type BoltAuthResult struct {
	Authenticated bool     // Whether authentication succeeded
	Username      string   // Authenticated username
	Roles         []string // User roles (admin, editor, viewer, etc.)
	Permissions   []string // Effective entitlement IDs (when set, used by HasPermission; else fallback to rolePerms)
}

BoltAuthResult contains the result of Bolt authentication.

func (*BoltAuthResult) HasPermission

func (r *BoltAuthResult) HasPermission(perm string) bool

HasPermission checks if the auth result has a specific permission. When Permissions is set (from role entitlements store), uses that list; else falls back to auth.RolePermissions.

func (*BoltAuthResult) HasRole

func (r *BoltAuthResult) HasRole(role string) bool

HasRole checks if the auth result has a specific role.

type BoltAuthenticator

type BoltAuthenticator interface {
	// Authenticate validates credentials from the Bolt HELLO message.
	// Returns auth result on success, error on failure.
	// scheme: "basic", "bearer", or "none"
	// principal: username (basic), empty (bearer/none)
	// credentials: password (basic), JWT token (bearer), empty (none)
	Authenticate(scheme, principal, credentials string) (*BoltAuthResult, error)
}

BoltAuthenticator is the interface for authenticating Bolt protocol connections. This supports Neo4j-compatible authentication schemes:

  • "basic": Username/password authentication
  • "bearer": JWT token authentication (for cluster inter-node auth)
  • "none": Anonymous access (if allowed)

The Bolt protocol HELLO message contains authentication credentials:

  • scheme: "basic", "bearer", or "none"
  • principal: username (basic) or empty (bearer/none)
  • credentials: password (basic) or JWT token (bearer)

For cluster deployments, use "bearer" scheme with a shared JWT secret:

# Generate cluster token on any node:
curl -X POST http://node1:7474/api/v1/auth/cluster-token \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"node_id": "node-2", "role": "admin"}'

# Use token to connect from other nodes:
driver = GraphDatabase.driver("bolt://node1:7687",
    auth=("", token))  # scheme=bearer when principal is empty

Example Implementation:

type MyAuthenticator struct {
	auth *auth.Authenticator
}

func (a *MyAuthenticator) Authenticate(scheme, principal, credentials string) (*BoltAuthResult, error) {
	switch scheme {
	case "none":
		if a.allowAnonymous {
			return &BoltAuthResult{Authenticated: true, Roles: []string{"viewer"}}, nil
		}
		return nil, fmt.Errorf("anonymous auth not allowed")
	case "bearer":
		claims, err := a.auth.ValidateToken(credentials)
		if err != nil {
			return nil, err
		}
		return &BoltAuthResult{Authenticated: true, Username: claims.Username, Roles: claims.Roles}, nil
	case "basic":
		// ... username/password validation
	}
}

type ClientAuthMode added in v1.1.2

type ClientAuthMode int

ClientAuthMode selects the TLS client-authentication policy applied to the Bolt listener. It maps onto tls.ClientAuthType.

const (
	// ClientAuthNone disables client-cert handling (tls.NoClientCert).
	ClientAuthNone ClientAuthMode = iota
	// ClientAuthRequest requests but does not require a client cert
	// (tls.RequestClientCert).
	ClientAuthRequest
	// ClientAuthRequestVerify requests a client cert; if presented, it is
	// verified (tls.VerifyClientCertIfGiven).
	ClientAuthRequestVerify
	// ClientAuthRequireVerify requires and verifies a client cert
	// (tls.RequireAndVerifyClientCert).
	ClientAuthRequireVerify
)

func ParseClientAuthMode added in v1.1.2

func ParseClientAuthMode(s string) (ClientAuthMode, error)

ParseClientAuthMode parses a textual ClientAuthMode. The empty string and "none" map to ClientAuthNone. Unknown values return an error.

type Config

type Config struct {
	Host            string
	Port            int
	MaxConnections  int
	ReadBufferSize  int
	WriteBufferSize int
	LogQueries      bool // Log all queries to stdout (for debugging)
	// ServerAnnouncement overrides the Bolt HELLO SUCCESS "server" metadata.
	// Leave empty to advertise NornicDB natively.
	ServerAnnouncement string

	// Authentication
	Authenticator  BoltAuthenticator // Authentication handler (nil = no auth)
	RequireAuth    bool              // Require authentication for all connections
	AllowAnonymous bool              // Allow "none" auth scheme (grants viewer role)

	// Logger is the structured-logging entrypoint per D-01. If nil, a
	// discard-handler fallback (D-01a) is installed at
	// NewWithDatabaseManager() so existing callers compile unchanged. The
	// Bolt HELLO message's "credentials" field is auto-redacted by the
	// Plan 02-01 redactingHandler chain (D-03a) — DefaultRedactKeys
	// already includes "credentials", so per-call scrubbing is not
	// required in pkg/bolt.
	Logger *slog.Logger

	// TLSConfig, when non-nil, enables TLS-on-first-byte sniffing on the
	// Bolt port. Mirrors Neo4j's sslContext + requiresEncryption pattern:
	// one listener accepts plain bolt://, bolt+s://, ws://, and wss://
	// based on the first 5 bytes of the connection.
	TLSConfig *tls.Config
	// RequireTLS rejects any non-TLS connection on the Bolt port with the
	// canonical Neo4j error message.
	RequireTLS bool
	// BoltSniffTimeout bounds the transport-sniff peek (default 5s).
	BoltSniffTimeout time.Duration
	// BoltAuthTimeout bounds the pre-HELLO handshake/auth window after
	// transport selection (default 30s, matches Neo4j).
	BoltAuthTimeout time.Duration
	// BoltStatementTimeout bounds the wall-clock duration of a single RUN
	// when the client did not supply tx_timeout in the RUN/BEGIN extras.
	// Zero (the default) disables the server-side cap and matches the
	// historical behavior. The Bolt driver's per-call tx_timeout, when
	// present, takes precedence (Neo4j semantics: client-supplied
	// timeout wins; server cap is the fallback). The cancellation
	// propagates via context to the Cypher executor, which honors
	// ctx.Err() at every traversal/match boundary.
	BoltStatementTimeout time.Duration
	// WebSocketEnabled controls whether WebSocket transport is accepted on
	// the Bolt port. When false, the server returns the discovery response
	// for plain GET / and HTTP 426 for actual WS upgrade attempts.
	// Defaults to true.
	WebSocketEnabled bool
	// WebSocketAllowedOrigins is a comma-separated list of allowed Origin
	// header values for WS upgrades. Use "*" for any origin.
	WebSocketAllowedOrigins string
	// WebSocketMaxMessageSize bounds inbound WS BinaryMessage size
	// (default 65536, matches Neo4j MAX_WEBSOCKET_FRAME_SIZE).
	WebSocketMaxMessageSize int64
	// WebSocketWriteBufferSize is the bufio writer buffer size used for
	// sessions whose transport is WS or WS+TLS (default 256 KB).
	WebSocketWriteBufferSize int
	// WebSocketPingInterval is the cadence at which the server sends WS
	// ping control frames (default 30s).
	WebSocketPingInterval time.Duration
	// WebSocketPongTimeout is the deadline within which a pong must
	// arrive after a ping (default 60s).
	WebSocketPongTimeout time.Duration
	// OAuthConfig optionally provides the OAuth/OIDC discovery payload
	// emitted on plain GET / probes. When nil or unconfigured, the
	// discovery body is empty (Community-parity).
	OAuthConfig *auth.OAuthConfig
}

Config holds Bolt protocol server configuration.

All settings have sensible defaults via DefaultConfig(). The configuration follows Neo4j Bolt server conventions where applicable.

Authentication:

  • Set Authenticator to enable auth (nil = no auth, accepts all)
  • RequireAuth: if true, connections without valid credentials are rejected
  • AllowAnonymous: if true, "none" auth scheme is accepted (viewer role)

Example:

// Production configuration with auth
config := &bolt.Config{
	Port:            7687,  // Standard Bolt port
	MaxConnections:  1000,  // High concurrency
	ReadBufferSize:  32768, // 32KB read buffer
	WriteBufferSize: 32768, // 32KB write buffer
	Authenticator:   myAuth,
	RequireAuth:     true,
}

// Development configuration (no auth)
config = bolt.DefaultConfig()
config.Port = 7688 // Use different port

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns Neo4j-compatible default Bolt server configuration.

Defaults match Neo4j Bolt server settings:

  • Port 7687 (standard Bolt port)
  • 100 max concurrent connections
  • 8KB read/write buffers

Example:

config := bolt.DefaultConfig()
server := bolt.New(config, executor)

type DatabaseManagerInterface

type DatabaseManagerInterface interface {
	GetStorage(name string) (storage.Engine, error)
	Exists(name string) bool
	DefaultDatabaseName() string
}

DatabaseManagerInterface provides database management without importing multidb.

type DeferrableExecutor

type DeferrableExecutor interface {
	FlushableExecutor
	// SetDeferFlush enables/disables deferred flush mode.
	SetDeferFlush(enabled bool)
}

DeferrableExecutor extends FlushableExecutor with deferred flush mode control.

type FlushableExecutor

type FlushableExecutor interface {
	QueryExecutor
	// Flush persists all pending writes to storage.
	Flush() error
}

FlushableExecutor extends QueryExecutor with deferred commit support. This enables Neo4j-style optimization where writes are buffered until PULL.

type QueryExecutor

type QueryExecutor interface {
	Execute(ctx context.Context, query string, params map[string]any) (*QueryResult, error)
}

QueryExecutor executes Cypher queries for the Bolt server.

This interface allows the Bolt server to be decoupled from the specific database implementation. The executor receives Cypher queries and parameters from Bolt clients and returns results in a standard format.

Example Implementation:

type MyExecutor struct {
	db *nornicdb.DB
}

func (e *MyExecutor) Execute(ctx context.Context, query string, params map[string]any) (*QueryResult, error) {
	// Execute query against NornicDB
	result, err := e.db.ExecuteCypher(ctx, query, params)
	if err != nil {
		return nil, err
	}

	// Convert to Bolt format
	return &QueryResult{
		Columns: result.Columns,
		Rows:    result.Rows,
	}, nil
}

The executor should handle:

  • Cypher query parsing and execution
  • Parameter substitution
  • Result formatting
  • Error handling and reporting

type QueryResult

type QueryResult struct {
	Columns  []string
	Rows     [][]any
	Metadata map[string]any
	Stats    *QueryStats // Write counters for ResultSummary
}

QueryResult holds the result of a query.

type QueryStats added in v1.1.5

type QueryStats struct {
	NodesCreated         int
	NodesDeleted         int
	RelationshipsCreated int
	RelationshipsDeleted int
	PropertiesSet        int
	LabelsAdded          int
}

QueryStats holds write counters emitted in the Bolt PULL completion metadata. Field names match the Neo4j Bolt protocol specification for "stats".

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server implements a Neo4j Bolt protocol server for NornicDB.

The server handles multiple concurrent client connections, each running in its own goroutine. It manages the Bolt protocol handshake, authentication, and message routing to the configured query executor.

Example:

config := bolt.DefaultConfig()
executor := &MyExecutor{} // Implements QueryExecutor
server := bolt.New(config, executor)

go func() {
	if err := server.ListenAndServe(); err != nil {
		// Operators should route this through the structured logger
		// returned by observability.NewLogger; the server itself
		// announces its listening address via slog at INFO once
		// ListenAndServe binds successfully.
		_ = err
	}
}()

// Server is now accepting connections (announce log emitted by the
// server's own structured logger).

Thread Safety:

The server is thread-safe and handles concurrent connections safely.

func New

func New(config *Config, executor QueryExecutor) *Server

New creates a new Bolt protocol server with the given configuration and executor.

Parameters:

  • config: Server configuration (uses DefaultConfig() if nil)
  • executor: Query executor for handling Cypher queries (required if dbManager is nil)
  • dbManager: Database manager for multi-database support (optional, if nil uses executor)

Returns:

  • Server instance ready to start

Note: If dbManager is provided, it takes precedence and executor is ignored. The dbManager enables multi-database support where each connection can specify a database in the HELLO message.

Example:

config := bolt.DefaultConfig()
executor := &MyQueryExecutor{db: nornicDB}
server := bolt.New(config, executor)

// Start server
if err := server.ListenAndServe(); err != nil {
	log.Fatal(err)
}

Example 1 - Basic Setup with Cypher Executor:

// Create storage engine
storage := storage.NewBadgerEngine("./data/nornicdb")
defer storage.Close()

// Create Cypher executor
cypherExec := cypher.NewStorageExecutor(storage)

// Create Bolt server
config := bolt.DefaultConfig()
config.Port = 7687

server := bolt.New(config, cypherExec)

// Start server (blocks until shutdown)
log.Fatal(server.ListenAndServe())

Example 2 - Production with Connection Limits:

config := bolt.DefaultConfig()
config.Port = 7687
config.MaxConnections = 500     // Handle 500 concurrent clients
config.ReadBufferSize = 8192    // 8KB buffer
config.WriteBufferSize = 8192
config.IdleTimeout = 10 * time.Minute

executor := cypher.NewStorageExecutor(storage)
server := bolt.New(config, executor)

// Graceful shutdown — operators wire this through lifecycle.Run so
// the slog-bound logger emits the shutdown notice with structured
// component=bolt attribution.
go func() {
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
	<-sigChan
	server.Close()
}()

if err := server.ListenAndServe(); err != nil {
	log.Fatal(err)
}

Example 3 - Custom Query Executor with Middleware:

// Create custom executor with auth and logging
type AuthExecutor struct {
	inner cypher.Executor
	auth  *auth.Authenticator
	audit *audit.Logger
}

func (e *AuthExecutor) Execute(ctx context.Context, query string, params map[string]any) (*bolt.QueryResult, error) {
	// Extract user from context
	user := ctx.Value("user").(string)

	// Audit log
	e.audit.LogDataAccess(user, user, "query", query, "EXECUTE", true, "")

	// Execute query
	result, err := e.inner.Execute(ctx, query, params)

	// Convert to Bolt result format
	return &bolt.QueryResult{
		Columns: result.Fields,
		Rows:    result.Records,
	}, err
}

executor := &AuthExecutor{
	inner: cypher.NewStorageExecutor(storage),
	auth:  authenticator,
	audit: auditLogger,
}

server := bolt.New(bolt.DefaultConfig(), executor)
server.ListenAndServe()

Example 4 - Testing with In-Memory Storage:

func TestMyBoltIntegration(t *testing.T) {
	// In-memory storage for tests
	storage := storage.NewMemoryEngine()
	executor := cypher.NewStorageExecutor(storage)

	// Bolt server on random port
	config := bolt.DefaultConfig()
	config.Port = 0 // OS assigns random available port

	server := bolt.New(config, executor)

	// Start server in background
	go server.ListenAndServe()
	defer server.Close()

	// Connect with Neo4j driver
	driver, _ := neo4j.NewDriver(
		fmt.Sprintf("bolt://localhost:%d", server.Port()),
		neo4j.NoAuth(),
	)
	defer driver.Close()

	// Run test queries
	session := driver.NewSession(neo4j.SessionConfig{})
	result, _ := session.Run("CREATE (n:Test {value: 42}) RETURN n", nil)
	// ... assertions ...
}

ELI12:

Think of the Bolt server like a translator at the UN:

  • Neo4j drivers speak "Bolt language" (binary protocol)
  • NornicDB speaks "Cypher language" (graph queries)
  • The Bolt server translates between them!

Why do we need this translator?

  1. Neo4j drivers already exist (Python, Java, JavaScript, Go, etc.)
  2. Tools like Neo4j Browser, Bloom, and Cypher Shell work out of the box
  3. No need to write new drivers for every programming language

How it works:

  1. Driver connects: "Hi, I speak Bolt 4.3"
  2. Server responds: "Cool, I understand Bolt 4.3"
  3. Driver sends: "RUN: MATCH (n) RETURN n LIMIT 10"
  4. Server executes Cypher and sends back results
  5. Driver receives results in Bolt format

Real-world analogy:

  • HTTP is like writing letters (text-based, verbose)
  • Bolt is like speaking on the phone (binary, efficient)
  • Bolt is ~3-5x faster than HTTP for graph queries!

Compatible Tools:

  • Neo4j Browser (web UI)
  • Neo4j Desktop
  • Cypher Shell (CLI)
  • Neo4j Bloom (graph visualization)
  • Any app using Neo4j drivers

Protocol Advantages:

  • Binary format (smaller, faster)
  • Connection pooling (reuse connections)
  • Streaming results (low memory)
  • Transaction support (BEGIN/COMMIT/ROLLBACK)
  • Pipelining (send multiple queries without waiting)

Performance:

  • Handles 100-500 concurrent connections easily
  • ~1ms overhead per query
  • Streaming results use O(1) memory per connection
  • Binary PackStream is ~40% smaller than JSON

Thread Safety:

Server handles concurrent connections safely.

func NewWithDatabaseManager

func NewWithDatabaseManager(config *Config, executor QueryExecutor, dbManager DatabaseManagerInterface) *Server

NewWithDatabaseManager creates a new Bolt protocol server with multi-database support.

Parameters:

  • config: Server configuration (uses DefaultConfig() if nil)
  • executor: Query executor (ignored if dbManager is provided, kept for backward compatibility)
  • dbManager: Database manager for multi-database support (optional)

Returns:

  • Server instance ready to start

If dbManager is provided, queries are routed to the correct database based on the "db" or "database" parameter in the HELLO message. If not provided, the server uses the single executor for all queries (backward compatible).

func (*Server) Close

func (s *Server) Close() error

Close stops the Bolt server.

func (*Server) IsClosed

func (s *Server) IsClosed() bool

IsClosed returns whether the server is closed.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe() error

ListenAndServe starts the Bolt server and begins accepting connections.

The server listens on the configured port and handles incoming Bolt connections. Each connection is handled in a separate goroutine.

Returns:

  • nil if server shuts down cleanly
  • Error if failed to bind to port or other startup error

Example:

server := bolt.New(config, executor)

// Start server (blocks until shutdown)
if err := server.ListenAndServe(); err != nil {
	log.Fatalf("Bolt server failed: %v", err)
}

The server will print its listening address when started successfully.

func (*Server) SetAuthMetrics added in v1.1.0

func (s *Server) SetAuthMetrics(bag *observability.AuthMetrics)

SetAuthMetrics injects the Plan-04-06 Auth catalog bag for the auth_attempts_total{result, protocol="bolt"} crosswire (D-05e + D-11). Plan 04-02 ships the call site behind a nil-check; Plan 04-06 wires the bag at cmd/nornicdb startup.

func (*Server) SetBoltMetrics added in v1.1.0

func (s *Server) SetBoltMetrics(bag *observability.BoltMetrics)

SetBoltMetrics injects the Plan-04-02 Bolt catalog bag (D-02 typed handle DI) plus pre-built per-op bound observers (MET-25). MUST be called BEFORE ListenAndServe. Nil-safe: passing nil leaves the server in metrics-disabled mode (matches existing test fixtures).

Pairs with SetAuthMetrics for the D-11 auth-attempts crosswire (Plan 04-06 wires the Auth bag).

func (*Server) SetDatabaseAccessMode

func (s *Server) SetDatabaseAccessMode(mode auth.DatabaseAccessMode)

SetDatabaseAccessMode sets the per-database access mode (e.g. from HTTP server). When set, CanAccessDatabase(dbName) is checked before running each query.

func (*Server) SetDatabaseAccessModeResolver

func (s *Server) SetDatabaseAccessModeResolver(resolver func(roles []string) auth.DatabaseAccessMode)

SetDatabaseAccessModeResolver sets a per-principal resolver (e.g. from HTTP server for Phase 3 allowlist). When set, the resolver is called with the session's roles to get the mode for each query.

func (*Server) SetResolvedAccessResolver

func (s *Server) SetResolvedAccessResolver(resolver func(roles []string, dbName string) auth.ResolvedAccess)

SetResolvedAccessResolver sets a per-(principal, db) resolver for Phase 4 write checks.

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session represents a client session.

type SessionExecutorFactory

type SessionExecutorFactory interface {
	NewSessionExecutor() QueryExecutor
}

SessionExecutorFactory creates per-connection executors. For explicit transactions, each call must return a distinct TransactionalExecutor; a shared instance cannot safely represent concurrent transaction ownership.

type TransactionalExecutor

type TransactionalExecutor interface {
	QueryExecutor
	BeginTransaction(ctx context.Context, metadata map[string]any) error
	CommitTransaction(ctx context.Context) error
	RollbackTransaction(ctx context.Context) error
}

TransactionalExecutor extends QueryExecutor with transaction support.

Atomic BEGIN/COMMIT/ROLLBACK requires NewWithDatabaseManager or a SessionExecutorFactory that returns a distinct TransactionalExecutor for each connection. A directly supplied TransactionalExecutor is supported only with MaxConnections set to 1 and is quarantined after cleanup failure or an uncertain commit outcome. Multi-connection servers reject BEGIN for a shared raw executor. Without this interface, transaction messages are acknowledged for compatibility but operations are auto-committed.

A factory can adapt an application-specific constructor without sharing its transaction field between connections:

type TxExecutorFactory struct {
	QueryExecutor
	newExecutor func() TransactionalExecutor
}

func (f *TxExecutorFactory) NewSessionExecutor() QueryExecutor {
	return f.newExecutor()
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL