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:
- Connection accept (handleConnection in server.go): Inc/Dec ConnectionsActive gauge; observe SessionDuration on close; increment ConnectionsTotal{result} on close.
- Per-message dispatch loop (handleMessage / dispatchMessage in server.go): observe MessageDuration{op} per message; increment MessagesTotal{op, result}.
- 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:
**Different languages**: Neo4j drivers speak "Bolt language" but NornicDB speaks "NornicDB language". The Bolt server translates between them.
**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!
**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!
**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
- Variables
- func LoadTLSConfig(certFile, keyFile string) (*tls.Config, error)
- func LoadTLSConfigWithClientCA(certFile, keyFile, clientCAFile string, mode ClientAuthMode) (*tls.Config, error)
- type AuthenticatorAdapter
- type BoltAuthResult
- type BoltAuthenticator
- type ClientAuthMode
- type Config
- type DatabaseManagerInterface
- type DeferrableExecutor
- type FlushableExecutor
- type QueryExecutor
- type QueryResult
- type QueryStats
- type Server
- func (s *Server) Close() error
- func (s *Server) IsClosed() bool
- func (s *Server) ListenAndServe() error
- func (s *Server) SetAuthMetrics(bag *observability.AuthMetrics)
- func (s *Server) SetBoltMetrics(bag *observability.BoltMetrics)
- func (s *Server) SetDatabaseAccessMode(mode auth.DatabaseAccessMode)
- func (s *Server) SetDatabaseAccessModeResolver(resolver func(roles []string) auth.DatabaseAccessMode)
- func (s *Server) SetResolvedAccessResolver(resolver func(roles []string, dbName string) auth.ResolvedAccess)
- type Session
- type SessionExecutorFactory
- type TransactionalExecutor
Constants ¶
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
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 ¶
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.
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
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:
Configure all nodes with the same JWT secret: NORNICDB_JWT_SECRET=your-shared-secret-min-32-bytes
Generate a cluster token on any node: POST /api/v1/auth/cluster-token {"node_id": "node-2", "role": "admin"}
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?
- Neo4j drivers already exist (Python, Java, JavaScript, Go, etc.)
- Tools like Neo4j Browser, Bloom, and Cypher Shell work out of the box
- No need to write new drivers for every programming language
How it works:
- Driver connects: "Hi, I speak Bolt 4.3"
- Server responds: "Cool, I understand Bolt 4.3"
- Driver sends: "RUN: MATCH (n) RETURN n LIMIT 10"
- Server executes Cypher and sends back results
- 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) ListenAndServe ¶
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()
}
Source Files
¶
- auth_adapter.go
- committed_write_cache.go
- discovery.go
- metrics.go
- packstream.go
- packstream_metrics.go
- server.go
- session_messages.go
- session_transaction.go
- spans.go
- tls.go
- transaction_cleanup.go
- transaction_executor_adapter.go
- transaction_lifecycle.go
- transaction_timeout_metadata.go
- transport_discovery.go
- transport_metrics.go
- transport_select.go
- transport_ws.go
- wsconn.go