Architecture Overview¶
This guide provides a comprehensive overview of the Neo4j Enterprise Operator's architecture, design principles, and current implementation.
Core Design Principles¶
The Neo4j Enterprise Operator follows cloud-native best practices with a focus on:
- Production Stability: Optimized reconciliation frequency and efficient resource management
- Performance: Intelligent rate limiting and status update optimization
- Server-Based Architecture: Unified server deployments with self-organizing roles
- Resource Efficiency: On-demand backup Jobs (no long-running backup pod or sidecar)
- Observability: Prometheus metrics, Kubernetes events and conditions, and live
SHOW SERVERS/SHOW DATABASESdiagnostics instatus - Validation: Proactive resource validation and recommendations
Current Architecture¶
Server-Based Architecture¶
The operator has evolved to use a unified server-based architecture where Neo4j servers self-organize into primary/secondary roles:
Key Changes from Legacy Architecture¶
- Before: Separate primary/secondary StatefulSets with complex orchestration
- After: Single
{cluster-name}-serverStatefulSet with self-organizing servers - Benefit: Simplified resource management, improved scaling, reduced complexity
Current Implementation¶
# Neo4jEnterpriseCluster topology
topology:
servers: 3 # Creates: my-cluster-server StatefulSet (replicas: 3)
# Pods: my-cluster-server-0, my-cluster-server-1, my-cluster-server-2
# Neo4jEnterpriseStandalone deployment
# Creates: my-standalone StatefulSet (replicas: 1)
# Pod: my-standalone-0
Backup architecture¶
Backups are owned by the dedicated Neo4jBackup CRD: each CR (one-shot or scheduled via spec.schedule) spawns a Kubernetes Job (or CronJob → child Jobs) that runs neo4j-admin database backup from the same Neo4j Enterprise image as the cluster. No sidecar containers, no persistent backup pod. The Job connects to each {cluster}-server-N Pod on port 6362 (server.backup.listen_address=0.0.0.0:6362, configured automatically) and streams artifacts to the destination (PVC, S3, GCS, or Azure).
All runs of a single Neo4jBackup CR share a <base>/<chain-root>/ directory (the chain root is the CR name unless spec.chainFromBackup names another backup) so neo4j-admin can chain --type=DIFF backups off the prior FULL. Per-run identity is preserved via the timestamp neo4j-admin embeds in each .backup filename.
Custom Resource Definitions (CRDs)¶
The operator defines 27 CRDs in api/v1beta1/ (ls api/v1beta1/*_types.go; mcp_types.go holds shared types only). This page details the core workload, database, backup/restore and identity CRDs. Neo4jCompositeDatabase, Neo4jDatabaseAlias, Neo4jReplicaDatabase / Neo4jReplicaPromotion (cross-cluster replication) and the 12 Aura* CRDs are covered by their user guides and docs/api_reference/ (and internal/controller/CLAUDE.md lists every controller). The cluster-targeted ones follow the same patterns described below (clusterRef, inline validation, Cypher25()/Cypher5() for version-sensitive DDL); the Aura ones talk to the Aura REST API instead of Bolt.
The CRDs detailed here:
Core Deployment CRDs¶
Neo4jEnterpriseCluster (neo4jenterprisecluster_types.go)¶
- Purpose: High-availability clustered Neo4j Enterprise deployments
- Architecture: Server-based with
{cluster-name}-serverStatefulSet - Minimum Topology: 2+ servers (enforced by validation)
- Server Organization: Servers self-organize into primary/secondary roles for databases
- Scaling: Horizontal scaling supported with topology validation
- Discovery: LIST resolver with static pod FQDNs; V2_ONLY explicitly set for 5.26.x, implicit for 2025.x+
- Resource Pattern: Single StatefulSet replaces complex multi-StatefulSet architecture
Key Fields:
type Neo4jEnterpriseClusterSpec struct {
Image ImageSpec `json:"image"`
Topology TopologyConfiguration `json:"topology"` // servers: N
Storage StorageSpec `json:"storage"`
// Outbound TLS trust + escape hatches (see "Security Architecture
// > 6. Outbound trust" further down for the full lifecycle):
TrustedCASecrets []TrustedCASecret `json:"trustedCASecrets,omitempty"`
ExtraVolumes []corev1.Volume `json:"extraVolumes,omitempty"`
ExtraVolumeMounts []corev1.VolumeMount `json:"extraVolumeMounts,omitempty"`
// ... additional fields
}
Neo4jEnterpriseStandalone (neo4jenterprisestandalone_types.go)¶
- Purpose: Single-node Neo4j Enterprise deployments
- Architecture: Uses clustering infrastructure but fixed at 1 replica
- Use Cases: Development, testing, simple production workloads
- StatefulSet:
{standalone-name}(no "-server" suffix) - Configuration: Modern clustering approach with single member (Neo4j 5.26+)
- Restrictions: Cannot scale beyond 1 replica
- Shared API surface: Mirrors the cluster spec for
TrustedCASecrets/ExtraVolumes/ExtraVolumeMounts. The wire-up differs slightly: cluster pods receive-Djavax.net.ssl.trustStore=…via theNEO4J_server_jvm_additionalenv var, while standalone pods receive the same flags asserver.jvm.additional=…lines emitted into the ConfigMap-backed neo4j.conf.
Database Management CRDs¶
Neo4jDatabase (neo4jdatabase_types.go)¶
- Purpose: Manages database lifecycle within clusters and standalone deployments
- Dual Support: Works with both Neo4jEnterpriseCluster and Neo4jEnterpriseStandalone
- Enhanced Validation: DatabaseValidator supports automatic deployment type detection
- Neo4j 5.26+ Syntax: Uses modern
TOPOLOGYclause for database creation - Standalone Fix: Added NEO4J_AUTH environment variable for automatic authentication
Key Features:
type Neo4jDatabaseSpec struct {
ClusterRef string `json:"clusterRef"` // References cluster OR standalone
Name string `json:"name"` // Database name
Topology DatabaseTopology `json:"topology"` // Primary/secondary counts
IfNotExists bool `json:"ifNotExists"` // CREATE IF NOT EXISTS
}
Neo4jPlugin (neo4jplugin_types.go)¶
- Purpose: Manages Neo4j plugin installation and configuration
- Dual Architecture Support: Enhanced for server-based cluster + standalone compatibility
- Deployment Detection: Automatic cluster vs standalone recognition
- Resource Naming: Handles
{cluster-name}-servervs{standalone-name}patterns - Plugin Sources: Official, community, custom registry, direct URL support
Backup & Restore CRDs¶
Neo4jBackup (neo4jbackup_types.go)¶
- Purpose: Manages backup operations for both clusters and standalone deployments
- Job-per-CR architecture: Each
Neo4jBackup(one-shot or scheduled) spawns a Kubernetes Job (or CronJob → child Jobs). No sidecar containers, no persistent backup pod. - Target Support: Cluster OR standalone (the controller probes the
spec.instanceRefdeployment to resolve the type and routes toBuildStandaloneBackupFromAddressfor standalones,BuildBackupFromAddressesfor clusters) - Neo4j 5.26+ Support: Modern backup syntax with
--to-pathparameter - Shared-directory layout: All runs of a single CR write into
<base>/<chain-root>/(the CR name unlessspec.chainFromBackupsays otherwise) soneo4j-admincan chain differential backups off the prior full. Per-run identity is preserved via the ISO-8601 timestampneo4j-adminembeds in each.backupfilename (also captured intostatus.history[].artifactFilename/shardArtifacts[].filenamevia Pod-log parsing).
Neo4jRestore (neo4jrestore_types.go)¶
- Purpose: Manages database restoration from backups
- Point-in-Time Recovery: Supports
--restore-untilfor precise recovery - Cross-Deployment Support: Works with both
Neo4jEnterpriseClusterandNeo4jEnterpriseStandalone(auto-detected viaclusterRef) - Source types:
backup(reference aNeo4jBackupCR),storage(a PVC or cloud location),pitr(point-in-time).spec.source.typeis one ofbackup,storage,pitr; the S3/GCS/Azure/PVC choice isspec.source.storage.type, not a separate source type
Identity, Access & Sharding CRDs¶
Neo4jUser (neo4juser_types.go)¶
- Purpose: Declarative user lifecycle (create/update/drop) with password rotation via Secret hash
- Dual Support: Cluster or standalone (auto-detected;
clusterRefis namespace-scoped) - Authoritative spec: password (via Secret),
accountStatus,homeDatabase,roles[],externalAuth. Drift is reverted every reconcile. - No inline privileges: privileges live on
Neo4jRole, never onNeo4juser— users carry onlyroles: []bindings.
Neo4jRole (neo4jrole_types.go)¶
- Purpose: Declarative role + privilege management
- Authoritative spec:
privileges[]whenenforcePrivileges: true(default). Diff-based reconciliation viaSHOW ROLE ... PRIVILEGES AS COMMANDS; revokes are derived textually (DerivePrivilegeRevoke). - Built-in role guard: names in
{PUBLIC, reader, editor, publisher, architect, admin}rejected unlessspec.adoptBuiltin: true. Adopted built-ins are NEVER dropped on CR delete.
Neo4jRoleBinding (neo4jrolebinding_types.go)¶
- Purpose: Grants existing externally-provisioned users (SSO / LDAP first-login) into operator-managed roles, without creating or dropping the user
- Overlap guard: validator rejects bindings whose
clusterRef+usernamematch an existingNeo4jUserCR in the same namespace
Neo4jAuthRule (neo4jauthrule_types.go)¶
- Purpose: Attribute-based access control (ABAC) — declarative
AUTH RULEDDL - Version-gated: requires Neo4j 2026.03+ (
SupportsAuthRules); controller refuses to reconcile against older versions - Cypher: every statement prepends
CYPHER 25because system DB defaults to Cypher 5 on 2026.x, which can't parseAUTHkeywords
Neo4jShardedDatabase (neo4jshardeddatabase_types.go)¶
- Purpose: Sharded database management (property sharding via
db.shard.*) - Version-gated: requires Neo4j 2025.12+ images. The cluster must have enough servers to host the declared shard topology (graph shard + property shards), ideally 3+ for HA; per server the operator hard-rejects below 4GB memory / 1 CPU (8GB / 2 CPU recommended) —
NEO4J_SHARDING_RELAX_MEMORY_MIN=truedowngrades that to a warning for dev/test
Controllers Architecture¶
Core Controllers (internal/controller/)¶
Neo4jEnterpriseCluster Controller (neo4jenterprisecluster_controller.go)¶
Primary cluster management controller with server-based architecture:
Performance Optimizations:
- Efficient Reconciliation: Reduced from ~18,000 to ~34 reconciliations per minute
- Smart Status Updates: Only updates when cluster state changes
- ConfigMap Debouncing: 2-minute debounce prevents restart loops
- Resource Version Conflict Handling: Retry logic for concurrent updates
Server-Based Implementation:
- Single StatefulSet: Creates
{cluster-name}-serverinstead of separate primary/secondary - Self-Organizing Servers: Neo4j servers automatically assign database hosting roles
- Simplified Resource Management: Unified pod templates and configuration
- Certificate DNS: Includes all server pod names in TLS certificates
Split-Brain Detection:
- Location:
internal/controller/splitbrain_detector.go - Multi-Pod Analysis: Connects to each server to compare cluster views
- Automatic Repair: Restarts orphaned pods to rejoin main cluster
- Production Ready: Comprehensive logging and fallback mechanisms
Scale-Down Drain (internal/controller/scale_down.go):
Lowering spec.topology.servers does not immediately shrink the StatefulSet.
Deleting a pod that still hosts database copies orphans data and can break
quorum, so the controller drains the outgoing servers through Neo4j's own
server-lifecycle commands before lowering replicas.
- Replica hold: while a drain is in progress the controller skips writing
sts.Spec.Replicas(gated byscaleDownDrainInProgress+ theneo4j.com/scale-down-draining-serversannotation, a CSV of the outgoingserverIds). Targets are tracked by serverId, not pod ordinal, because a server'saddressgoes NULL once it entersDeallocating— an ordinal-based map would lose it and prematurely declare the drain complete. - State machine (
planScaleDownStepoverSHOW SERVERS):cordonevery outgoing server → once all areCordoned,DEALLOCATE DATABASES FROM SERVER(relocates their database copies) → wait while they still host any user database →DROP SERVERonce a server hosts onlysystem(a system-DB primary staysDeallocatingforever and never reachesDeallocated, so DROP is gated on hosting onlysystem, not on theDeallocatedstate) → finally lowerreplicas. Scaling back up mid-drain cancels it (clears the annotation). - Fail-fast pre-flight: before cordoning anything, a
DRYRUN DEALLOCATEchecks the move is feasible, and the system-DB voting floor (dbms.cluster.minimum_initial_system_primaries_count, read live viaMinimumSystemPrimaries) is enforced — if the requestedserversis below the floor, or a database has a sole primary on an outgoing server that Neo4j cannot relocate, the controller refuses with aScaleDownBlockedcondition and an actionable remedy instead of stranding a half-deallocated server. Neo4j has no primitive that relocates a sole primary (REALLOCATEanddeallocateDatabaseFromServerboth refuse it), so the operator never auto-changes a database'sTOPOLOGY— that would silently alter durability. - Formation floor:
EffectiveMinSystemPrimaries()writesminimum_initial_system_primaries_count = min(3, servers)at formation (overridable viaspec.topology.minSystemPrimaries, clamped to[2, servers]), so a cluster of 3+ can scale down to 3 (the recommended odd quorum) rather than being pinned at its birth size. - PVC retention: the server StatefulSet sets
persistentVolumeClaimRetentionPolicy: {whenScaled: Delete, whenDeleted: Retain}so a drained server's PVC is reclaimed on scale-down but cluster-wide deletion still retains data.
Neo4jEnterpriseStandalone Controller (neo4jenterprisestandalone_controller.go)¶
Single-node deployment controller:
Key Features:
- Clustering Infrastructure: Uses same infrastructure as clusters (Neo4j 5.26+ approach)
- Single Member Configuration: Sets up clustering with single server
- Resource Management: Handles ConfigMap, Service, and StatefulSet
- Status Tracking: Comprehensive status updates for standalone instances
Database Controller (neo4jdatabase_controller.go)¶
Enhanced for dual deployment support:
- Automatic Detection: Tries cluster lookup first, then standalone fallback
- Neo4j Client Creation:
NewClientForEnterprise()vsNewClientForEnterpriseStandalone() - Authentication Handling: Manages NEO4J_AUTH for standalone deployments
- Syntax Support: Neo4j 5.26+ and 2025.x database creation syntax
Plugin Controller (plugin_controller.go)¶
Manages plugin lifecycle with architecture compatibility:
- DeploymentInfo Abstraction: Unified handling of cluster/standalone types
- Resource Naming: Correct StatefulSet names (
{cluster-name}-servervs{standalone-name}) - Pod Labels: Applies appropriate labels for each deployment type
- Plugin Sources: Official, community, custom registries, direct URLs
Backup Controller (neo4jbackup_controller.go)¶
Backup management:
- Architecture: Job-per-
Neo4jBackup-CR. No persistent backup pod or sidecars. - Cross-Deployment Support: Backs up both clusters and standalone deployments
- Modern Syntax: Neo4j 5.26+ compatible backup commands
Restore Controller (neo4jrestore_controller.go)¶
Database restoration management:
- Point-in-Time Recovery: Supports precise timestamp restoration via
--restore-until - Flexible Targets: Cluster OR standalone (auto-detected through
getClusterRef; standalone is converted into a synthetic cluster representation withTopology.Servers=1viastandaloneAsCluster) - Validation: Ensures target deployment compatibility
Two restore paths. startRestore routes through restoreOnJobPath
(restore_target.go, rule 108): every cluster, and every standalone restore
whose seed the server can fetch itself, takes the online Cypher path (no
Job — neo4j-admin restore's --overwrite-destination is documented as
unsafe on a cluster, and on a standalone it stopped every database to restore
one). A standalone takes the Job path below only when
standaloneOfflineReason names a reason: point-in-time, source.type:
storage, or cloud storage by pod identity (standalone pods cannot carry
one). A PVC differential stays online: the seed proxy serves one file over
HTTP and a differential needs its chain, so an init container merges the
chain into one full artifact first (pvc_seed_merge.go, rule 109).
Online Cypher restore (startClusterCypherRestore, clusters and
standalones): no Job, no stopCluster/scale-down. The controller seeds each
server from a single backup-file URI — RecreateDatabaseWithSeedURI when the
database already exists, CreateDatabaseWithSeedURIOptions when it doesn't.
Because dbms.recreateDatabase is async, the controller then polls
SHOW DATABASE online-state across requeues via pollClusterRestoreOnline
before running post-restore hooks and marking the restore Completed. What
differs between the two kinds — the StatefulSet name, the pod labels the seed
proxy's NetworkPolicy admits, and the CR that carries the seed
spec.env/spec.extraEnvFrom — comes from restoreTarget.
Offline standalone restore lifecycle (Job path, post-Job-success):
-
Restore Job —
neo4j-admin database restoreruns in a Pod that mounts the server-0 data PVC (neo4j-data-{name}-0for standalone,data-{name}-server-0for the synthetic cluster representation — the only PVC the operator writes restored data to).--from-pathis resolved at Pod startup via shell substitution$(ls /backup/<run>/<dbname>-*.backup | tail -1)(tail -1picks the LATEST timestamped artifact) so a single artifact file is passed even when the directory holds multiple.backupfiles. Both the path and database name go throughshellQuote().--temp-path=/tmp/restore-tmpis defaulted for PVC sources because the backup PVC is mounted ReadOnly. -
Scale-up —
startClusterreads theneo4j.neo4j.com/original-replicasannotation off the StatefulSet and scales it back. The annotation is deleted on first successful scale-up; the second concurrent reconcile finds it missing and treats this as idempotent success (avoids a regression where a race terminal-failed the restore on a benign empty-annotation). -
Wait for Ready —
waitForClusterReadypolls status untilPhase=Ready. -
Database bring-up —
createOrStartDatabaseissuesCREATE DATABASE(if absent) orSTART DATABASE(if it existed and was stopped) via the Bolt client against the system DB. -
Multi-server re-seed (
recreateRestoredDatabaseOnCluster, gated onTopology.Servers >= 2) — callsdbms.[cluster.]recreateDatabase($db, {seedingServers: [$server0Id]})to force every server to re-sync from server-0's restored data. Server-0 is resolved viaSHOW SERVERS YIELD ... address, matched bycluster.Name + "-server-0". The procedure name is version-gated byversion.RecreateDatabaseProcedure(): dbms.cluster.recreateDatabase— SemVer 5.24+ (incl. 5.26 LTS), CalVer 2025.02–2025.03dbms.recreateDatabase— CalVer 2025.04+ and 2026.x+ (thecluster.*form was deprecated in 2025.04)- Empty string (skipped silently) — pre-5.24 SemVer / pre-2025.02 CalVer
The step is non-fatal — restore still transitions to Completed if the procedure fails, but emits a Warning event. In the Job path a standalone target has Topology.Servers=1, so this step is a no-op there; it survives for the synthetic-cluster representation only.
Standalone targets are auto-detected: getClusterRef accepts a standalone name and returns a synthetic Neo4jEnterpriseCluster via standaloneAsCluster (with Spec.Topology.Servers=1) so every downstream builder (command, env vars, volumes) works without a separate code path.
Standalone backup: neo4jbackup_controller.go::isStandaloneTarget detects when spec.instanceRef references a Neo4jEnterpriseStandalone. The --from address resolution switches from BuildBackupFromAddresses (cluster: comma-separated FQDNs) to BuildStandaloneBackupFromAddress (single FQDN). The rest of the backup flow — Job spec, PVC/cloud storage, shared <base>/<chain-root>/ directory, history population — is identical.
Integration test coverage: the test/integration suite covers cluster backup/restore (backup_*, restore_*, standard_database_*_restore) and the standalone Job-path all-databases round trip (all_databases_restore_standalone_test.go, extended). Single-database standalone backup/restore beyond that is exercised by unit tests in internal/controller/*_test.go and by the manual release verification walk (Phase 1).
Race-tolerance: AlreadyExists on Job creation is treated as "another reconcile got there first" rather than terminal-failure. Concurrent reconciles during the stopCluster cycle (10s scale-down delay queues a fresh reconcile before the original finishes) are common; without this tolerance the loser would flip the restore to Failed and the "Restore previously failed" guard would pin it permanently.
Neo4jUser / Neo4jRole / Neo4jRoleBinding Controllers (neo4juser_controller.go, neo4jrole_controller.go, neo4jrolebinding_controller.go)¶
Identity & access management:
- Shared cluster resolver:
cluster_resolver.go—ResolveClusterRefhandles cluster + standalone lookup - Watches: the user controller watches
Neo4jRoleso users referencing missing custom roles re-reconcile when the role lands (setsPendingDependenciescondition meanwhile, not terminal-fail) - Source-of-truth model: Privileges live on
Neo4jRole(never on users). Built-in roles requireadoptBuiltin: trueto be managed; never dropped on CR delete Neo4jRoleBindingis non-destructive: never creates or drops users — only grants/revokes role assignments on externally-provisioned (SSO / LDAP) users
Neo4jAuthRule Controller (neo4jauthrule_controller.go)¶
ABAC / attribute-based access control:
- Version requirement: refuses to reconcile against Neo4j versions where
SupportsAuthRules()returns false (pre-2026.03) - Production wiring: always present in
setupProductionControllers; dev mode (--controllers) MUST listauthruleor local deployments will silently acceptNeo4jAuthRuleCRs without reconciling them - Cypher prefix: every AUTH RULE DDL prepends
CYPHER 25(system DB defaults to Cypher 5 on 2026.x, which can't parseAUTHkeywords)
Neo4jShardedDatabase Controller (neo4jshardeddatabase_controller.go)¶
Property-sharding management:
- Version-gated: requires Neo4j 2025.12+ images
- Resource requirements: hard floor 4GB memory and 1 CPU per server (8GB / 2 CPU recommended), enforced in
validatePropertyShardingConfiguration(relaxable withNEO4J_SHARDING_RELAX_MEMORY_MIN=true, DEV/TEST only); 3+ servers recommended for HA
Validation Framework (internal/validation/)¶
Comprehensive Validation Architecture¶
Core Validators:¶
- TopologyValidator (
topology_validator.go): Cluster topology and server count validation - ClusterValidator (
cluster_validator.go): Cluster-specific configuration validation - MemoryValidator (
memory_validator.go): Neo4j memory settings vs container limits - ResourceValidator (
resource_validator.go): CPU, memory, and storage validation - TLSValidator (
tls_validator.go): TLS/SSL configuration validation - TruststoreValidator (
truststore_validator.go): unique Secret names inspec.trustedCASecrets(the name doubles as the keytool alias) plus reserved-mount-path collision check forspec.extraVolumeMounts(/data,/logs,/conf,/ssl,/plugins,/truststore,/truststore-ca,/var/lib/neo4j/...) - DatabaseValidator (
database_validator.go): Database creation and topology validation - AuthRuleValidator (
authrule_validator.go): Neo4jAuthRule name pattern + DDL-keyword guard on the condition expression (rejects CREATE / DROP / ALTER / GRANT / DENY / REVOKE / SHOW / RENAME and;injection) - RoleValidator / UserValidator / RoleBindingValidator (
role_validator.go,user_validator.go,rolebinding_validator.go): privilege list, identifier rules, cross-CR overlap withNeo4jUser
Enhanced Validation Features:¶
- Dual CRD Validation: Separate validation rules for cluster vs standalone
- Server-Based Topology: Validates server counts instead of primary/secondary counts
- Resource Recommendations: Suggests optimal resource allocation
- Configuration Restrictions: Prevents clustering settings in standalone deployments
- Neo4j Version Compatibility: Validates settings against Neo4j 5.26+ and 2025.x
Database Validator Enhancements¶
- Automatic Deployment Detection: Tries cluster first, then standalone
- Appropriate Client Creation: Uses correct client type for deployment
- Clear Error Messages: Distinguishes between cluster and standalone validation failures
Neo4j Version Compatibility¶
Supported Versions¶
- Neo4j 5.26.x: Last semver LTS release (5.26.0, 5.26.1, etc.) — no 5.27+ semver versions exist
- Neo4j 2025.x+: Calver format (2025.01.0, 2025.02.0, etc.)
Version-Specific Configuration¶
Discovery Configuration (LIST resolver, injected by startup script):¶
| Setting | 5.26.x (SemVer) | 2025.x+ / 2026.x+ (CalVer) |
|---|---|---|
dbms.cluster.discovery.resolver_type |
LIST |
LIST |
dbms.cluster.discovery.version |
V2_ONLY (explicit) |
(omitted — V2 is only protocol) |
| Endpoints key | dbms.cluster.discovery.v2.endpoints |
dbms.cluster.endpoints |
| Endpoint port | 6000 (tcp-tx) | 6000 (tcp-tx) |
| Bootstrap hint | internal.dbms.cluster.discovery.system_bootstrapping_strategy=me/other |
(not used) |
Port 5000 (tcp-discovery) is the deprecated V1 discovery port — never used by this operator.
CalVer detection: ParseVersion() → IsCalver (major >= 2025) covers 2026.x+ automatically.
Modern Configuration Standards:¶
- Memory:
server.memory.*(not deprecateddbms.memory.*) - TLS/SSL:
server.https.*andserver.bolt.*(notdbms.connector.*) - Database Format:
db.format: "block"(not deprecated formats) - Discovery: managed entirely by operator startup script — do not set in
spec.config
Version-gated Cypher procedures + flags:¶
| Feature | First available |
|---|---|
dbms.cluster.recreateDatabase (post-restore re-seed) |
SemVer 5.24+ (incl. 5.26 LTS), CalVer 2025.02–2025.03 |
dbms.recreateDatabase (replaces cluster.*) |
CalVer 2025.04+ and 2026.x+ |
AUTH RULE (ABAC) |
CalVer 2026.03+ |
Backup --parallel-download, --skip-recovery |
CalVer 2025.11+ |
Backup --remote-address-resolution |
CalVer 2025.09+ |
Backup --prefer-diff-as-parent |
CalVer 2025.04+ |
Restore --source-database filter |
CalVer 2025.02+ |
Picked at runtime by helpers in internal/neo4j/version.go (SupportsParallelDownload, SupportsAuthRules, RecreateDatabaseProcedure, etc.). Code paths that depend on these helpers MUST fall back gracefully when the version doesn't support the feature — never hard-fail a reconcile.
Database Creation Syntax¶
Neo4j 5.26+ (Cypher 5):¶
CREATE DATABASE name [IF NOT EXISTS]
[TOPOLOGY n PRIMAR{Y|IES} [m SECONDAR{Y|IES}]]
[OPTIONS "{" option: value[, ...] "}"]
[WAIT [n [SEC[OND[S]]]]|NOWAIT]
Neo4j 2025.x (Cypher 25):¶
CREATE DATABASE name [IF NOT EXISTS]
[[SET] DEFAULT LANGUAGE CYPHER {5|25}]
[[SET] TOPOLOGY n PRIMARIES [m SECONDARIES]]
[OPTIONS "{" option: value[, ...] "}"]
[WAIT [n [SEC[OND[S]]]]|NOWAIT]
Resource Management Architecture¶
Intelligent Resource Handling¶
Resource Builders (internal/resources/):¶
These are free Build*ForEnterprise / Build*ForStandalone functions, not stateful builder types. The standalone path reuses the cluster.go builders (StatefulSet, ConfigMap, Services) via the standaloneAsCluster synthetic representation rather than a dedicated standalone.go file.
- StatefulSet / Services / ConfigMap (
cluster.go):BuildServerStatefulSetForEnterprise,BuildClientServiceForEnterprise,BuildDiscoveryServiceForEnterprise,BuildConfigMapForEnterprise— server-based resources for both clusters and (via the synthetic cluster) standalone. - NetworkPolicy / Route / MCP (
networkpolicy.go,route.go,mcp.go): include*ForStandalonevariants for standalone-specific wiring. - Backup Job: built inline by
neo4jbackup_controller.go(per-Neo4jBackup-CR Kubernetes Job — no persistent backup pod, no sidecars, no dedicatedresources/builder). - TruststoreBuilder (
cluster.go:BuildTrustStoreInitContainer / BuildTrustStoreVolumes / CollectTrustedCASecrets): emits the per-Secret volume mounts, the writable/truststoreEmptyDir, and thetruststore-initinit container (seeds/truststore/truststore.jksfrom$JAVA_HOME/lib/security/cacerts, thenkeytool -importfor eachspec.trustedCASecretsentry using the Secret name as alias). Reused by both cluster (env var) and standalone (ConfigMap) wire-up paths viaCollectTrustedCASecrets, which folds the legacy singularspec.auth.trustStoreinto the new plural list.
Server-Based Resource Patterns:¶
- StatefulSet Naming:
{cluster-name}-serverfor clusters,{standalone-name}for standalone - Pod Naming:
{cluster-name}-server-0,{cluster-name}-server-1, etc. - Service Names:
{cluster-name}-headless,{cluster-name}-internals,{cluster-name}-client,{cluster-name}-metrics(when monitoring is on) - Backup Resources: per-CR Kubernetes Jobs spawned by the
Neo4jBackupcontroller (one-shot:<cr-name>-backup; CronJob children:<cr-name>-<unix-seconds>). No persistent backup pod or sidecars. - Truststore mount:
/truststore/truststore.jks(read-only, populated by thetruststore-initinit container; password is the JVM defaultchangeit) - User-supplied volumes:
spec.extraVolumesare appended to the pod spec verbatim;spec.extraVolumeMountsare appended to the Neo4j container's mounts after operator-managed mounts. Operator-managed paths (/data,/logs,/conf,/ssl,/plugins,/truststore,/truststore-ca,/var/lib/neo4j/...) are off-limits and rejected by the validator.
Performance Optimizations¶
Reconciliation Efficiency:¶
- Rate Limiting: Intelligent rate limiting prevents API server overload
- Status Update Efficiency: Only updates when state actually changes
- Event Filtering: Reduces unnecessary reconciliation triggers
- ConfigMap Hashing: Hash-based change detection prevents unnecessary updates
Startup Optimization:¶
- Parallel Pod Management: All server pods start simultaneously
minimum_initial_system_primaries_count = min(3, servers)(EffectiveMinSystemPrimaries(); overridable viaspec.topology.minSystemPrimaries, clamped to[2, servers]): Set only on initial cluster formation (when/data/databases/systemdoesn't exist). Makes RAFT wait for that many system-database primaries to be visible before electing a bootstrap leader — narrows the split-brain window when multiple pods come up in parallel. Skipped on restart so a single server can rejoin without waiting on its peers. It is also the floor a cluster can later be scaled down to (see Scale-Down Drain).- PublishNotReadyAddresses: Discovery includes pending pods
- Resource Version Conflict Retry: Handles concurrent updates gracefully
Security Architecture¶
RBAC Configuration (config/rbac/)¶
Core RBAC Resources:¶
- Principle of Least Privilege: Minimal required permissions
- ClusterRole Design: Cross-namespace operations support
- Service Account Security: Dedicated accounts with specific roles
Server pod identity:¶
Each cluster's server pods run under the ServiceAccount {cluster-name}-discovery
(the name is historical). It carries spec.podServiceAccountAnnotations for cloud
workload identity and is bound to no Role: LIST discovery resolves pod FQDNs
through DNS and never calls the Kubernetes API. Releases up to v1.20 also created a
{cluster-name}-discovery Role (get/list/watch on services and endpoints) and
RoleBinding; the cluster controller deletes the ones it owns (operations.md id 130).
TLS/SSL Support¶
The operator integrates with cert-manager for the full certificate lifecycle. The flow is the same for clusters and standalones, with one structural difference noted at the end.
1. Activation¶
TLS is opt-in via spec.tls:
Validation (internal/validation/tls_validator.go) runs inline during
reconciliation — no admission webhooks are used (CLAUDE.md "NO WEBHOOKS" hard constraint). When
mode: disabled, the entire TLS path is skipped and the deployment uses plain
bolt:// and http://.
2. Certificate creation¶
BuildCertificateForEnterprise (internal/resources/cluster.go) emits a
cert-manager.io/v1 Certificate whose dnsNames cover every endpoint clients
or peers may connect to:
- The client service (
{cluster}-client), the internals service ({cluster}-internals) and the headless service ({cluster}-headless), each at the short,.{ns},.svcand.svc.cluster.localforms - Each individual server pod under the internals and headless services (
{cluster}-server-0.{cluster}-headless.{ns}.svc.cluster.local, …) spec.service.dnsNameand the CCDR proxy / LoadBalancer hostname where applicable (a proxy IP goes inipAddresses, neverdnsNames)
The Certificate references the user-supplied issuerRef and writes its
material into a Secret named {resource-name}-tls-secret (tls.crt, tls.key,
ca.crt). cert-manager owns rotation; the operator never touches expiry.
3. Mounting into Neo4j pods¶
The StatefulSet builder mounts the Secret read-only at /ssl/
(internal/resources/cluster.go:~1208). Neo4j is then pointed at this directory
via server.directories.certificates=/ssl along with three SSL policies that
share the same key/cert/CA bundle:
dbms.ssl.policy.bolt.*— client traffic (bolt+s://)dbms.ssl.policy.https.*— Browser/HTTP APIdbms.ssl.policy.cluster.*— RAFT and discovery between server pods
The cluster SSL policy posture is governed by spec.tls.strictPeerValidation
(default true):
- Strict (default) — emits
trust_all=false,client_auth=REQUIRE(mutual TLS), andverify_hostname=true. The cert-manager Secret is projected viaSecret.items[]so itsca.crtlands at/ssl/trusted/ca.crt, which is the directory Neo4j's cluster SSL policy reads whentrust_all=false. Matches Neo4j's canonical SSL framework guidance. - Legacy (
strictPeerValidation: false) — reverts totrust_all=true client_auth=NONE. Provided as an opt-out for installations whose external issuer does not populateca.crt. Neo4j's own docs call this posture "debugging only, since it does not offer security." The controller-side preflightverifyTLSSecretHasCArefuses the strict config whenca.crtis missing and surfaces a clear status message rather than silently rolling the STS into a no-trust-anchor strict configuration.
When TLS is enabled, server.bolt.tls_level=REQUIRED is also set — plain
bolt:// connections are rejected (docs/knowledge/operations.md ids 9–13, TLS / Bolt client).
4. Operator-side Bolt connection (outgoing)¶
The operator reconciles the cluster by issuing Cypher commands over Bolt. Two separate concerns layer here: which scheme the URI uses (routing vs direct) and which CA the client trusts.
URI scheme — routing vs direct. buildConnectionURIForEnterprise
(internal/neo4j/client.go) uses the routing scheme:
| Spec | Scheme |
|---|---|
spec.tls.mode: disabled (or unset) |
neo4j:// |
spec.tls.mode: cert-manager |
neo4j+s:// |
The routing scheme is mandatory. Cluster admin commands (CREATE/DROP USER,
GRANT/REVOKE, CREATE/ALTER/DROP DATABASE, AUTH RULE management, etc.) must
execute on the cluster leader; the Go driver routes write transactions to
the leader only under neo4j://. Under the direct bolt:// scheme,
AccessMode: AccessModeWrite is silently ignored and connections land
wherever K8s steered them via the {cluster}-client ClusterIP. The
operator's Bolt clients used to use bolt://, which produced
Neo.ClientError.Cluster.NotALeader on N-1 of every N reconciles and
visible Ready ↔ Failed status flicker on the role/user/auth-rule
controllers. See docs/knowledge/operations.md id 11.
The single legitimate bolt:// consumer is
internal/controller/splitbrain_detector.go:createPodSpecificNeo4jClient,
which intentionally bypasses the routing layer to query each pod's RAFT
view individually — the whole point of split-brain detection is to compare
per-pod state, not to talk to the leader. Standalone deployments use the
routing scheme too, for symmetry; on a single-member topology
getRoutingTable reports the lone member as both reader and writer, so
behavior is equivalent to direct connection.
Driver timeouts. NewClientForEnterprise and NewClientForPod configure
(the standalone client deliberately uses larger 30s / 15s / 30s values for
startup tolerance — NewClientForEnterpriseStandalone):
ConnectionAcquisitionTimeout = 10s— full budget for getting a connection (includes routing-table fetch retries underneo4j://)SocketConnectTimeout = 5s— TCP connect to a router memberMaxTransactionRetryTime = 15s— retry budget for transient errors
These values are deliberately tight: an unreachable cluster fails fast
instead of stalling the controller's reconcile queue behind hung Bolt
calls. Healthy clusters complete the routing handshake in well under one
second. See docs/knowledge/operations.md id 12.
TLS. buildTLSConfig (internal/neo4j/client.go) governs which CA the
client trusts:
- Auto-discovery: load
ca.crtfrom the{resource-name}-tls-secretSecret and trust it as the CA for outgoing connections. This is the default path — no user configuration required. - Override:
spec.tls.trustedCASecretlets users point at a different Secret (e.g. when bringing their own CA outside cert-manager). - Fallback: with no usable
ca.crtthe client verifies by pinning the server certificate fromtls.crt(a one-certificate trust store, hostname still checked). With neither key it fails closed. There is noInsecureSkipVerifypath (docs/knowledge/operations.mdid 9).
All three Bolt entry points — NewClientForEnterprise,
NewClientForEnterpriseStandalone, and NewClientForPod (split-brain
detector) — go through buildTLSConfig, so the scheme switches
dynamically between TLS-enabled and plain variants based on spec.tls.mode
(docs/knowledge/operations.md id 10).
5. Standalone differences¶
Neo4jEnterpriseStandalone follows the same flow with two structural
differences:
- A single pod, so
dnsNamesis shorter (one server FQDN + the client service). - Neo4j configuration is delivered via a ConfigMap rather than StatefulSet env
vars; the
health.shprobe (mounted alongsideneo4j.confwith mode0755) also lives in this ConfigMap (docs/knowledge/operations.mdid 6).
6. Outbound trust — spec.trustedCASecrets & spec.extraVolumes¶
The cert-manager flow above governs Neo4j-the-server's inbound TLS (Bolt, HTTPS, intra-cluster RAFT). Neo4j also makes outbound TLS calls — to OIDC providers, LDAPS servers, Aura Fleet Management, plugin download mirrors, and in some cluster topologies to peer clusters for replication. When those endpoints use a CA the JDK doesn't trust by default, the operator wires a custom JVM truststore.
Sources of truth in code:
| Concern | Source |
|---|---|
| API types | api/v1beta1/neo4jenterprisecluster_types.go:TrustedCASecret, plus Neo4jEnterpriseClusterSpec.TrustedCASecrets / ExtraVolumes / ExtraVolumeMounts (mirrored on standalone) |
| Validation | internal/validation/truststore_validator.go (unique Secret names, reserved-mount-path collision check) |
| Init container + volumes | internal/resources/cluster.go:BuildTrustStoreInitContainer / BuildTrustStoreVolumes / CollectTrustedCASecrets |
| JVM-additional wire-up (cluster) | internal/resources/cluster.go — env var NEO4J_server_jvm_additional |
| JVM-additional wire-up (standalone) | internal/controller/neo4jenterprisestandalone_controller.go:createConfigMap — written as server.jvm.additional=... lines in the ConfigMap-backed neo4j.conf |
Init container flow (one container, runs before Neo4j, image is the same
Neo4j image so keytool is guaranteed to be present):
cp $JAVA_HOME/lib/security/cacerts /truststore/truststore.jks— seeds the writable JKS with the JDK's default trust roots so public CAs (Let's Encrypt, DigiCert, etc.) keep working. Without this seed step Neo4j would lose trust in public infrastructure when any custom CA was added.- For each
TrustedCASecret:keytool -import -trustcacerts -alias <secret-name> -file /trusted-ca/<secret-name>/<key>(default keyca.crtmatches the layout of cert-manager-issued Secrets). - The resulting JKS is mounted read-only at
/truststore/truststore.jksinto the main Neo4j container.
JVM args: -Djavax.net.ssl.trustStore=/truststore/truststore.jks
-Djavax.net.ssl.trustStorePassword=changeit — appended to whatever the user
supplied via spec.config["server.jvm.additional"]. Cluster pods receive
these via the NEO4J_server_jvm_additional env var; standalone pods receive
them as server.jvm.additional=... lines written into the ConfigMap-backed
neo4j.conf.
Backward compatibility: the older singular spec.auth.trustStore
(*SecretKeyRef) is folded into the new list at reconcile time via
CollectTrustedCASecrets. Both paths produce the same volumes, init
container, and JVM flags. Names from the explicit trustedCASecrets list
win on duplication.
Reserved paths for ExtraVolumeMounts: /data, /logs, /conf,
/ssl, /plugins, /truststore, /truststore-ca, /var/lib/neo4j (and
its data/, logs/, conf/, plugins/, certificates/ subdirectories)
are all rejected by the validator — silently overlaying them would either
destroy operator-managed content or fight the truststore-init flow.
Why this is needed for ABAC: Neo4j 2026.04 hard-requires https:// for
every dbms.security.oidc.<name>.* URI and rejects http:// at config-parse
time, before boot. Test environments and self-hosted OIDC providers therefore
need a TLS-fronted stub plus a trusted CA — trustedCASecrets is the
ergonomic path; extraVolumes is the escape hatch when a Neo4j SSL policy
references a per-policy truststore_path.
7. Adjacent integrations¶
- ExternalSecrets: when
spec.auth.adminSecretreferences a Secret managed by ExternalSecrets, the operator resolves it identically — TLS material remains under cert-manager's control regardless. Neo4jAuthRule(ABAC) and OIDC trust: the auth-rule reconciler talks to Neo4j via Bolt and does not directly interact with the JVM truststore. However, the cluster itself needs trust to fetch the OIDC well-known document — that's whattrustedCASecretsconfigures.
TLS/SSL quick reference¶
| Concern | Source |
|---|---|
| Validation | internal/validation/tls_validator.go, internal/validation/truststore_validator.go |
| Certificate CR shape | internal/resources/cluster.go:BuildCertificateForEnterprise |
/ssl/ mount + SSL policies |
internal/resources/cluster.go (~line 1208, ~line 1594) |
| Operator outgoing Bolt TLS | internal/neo4j/client.go:buildTLSConfig |
| Neo4j-server outgoing TLS truststore | internal/resources/cluster.go:BuildTrustStoreInitContainer (init container) + NEO4J_server_jvm_additional env var |
spec.trustedCASecrets API |
api/v1beta1/neo4jenterprisecluster_types.go:TrustedCASecret |
spec.extraVolumes / spec.extraVolumeMounts API |
same file, on the cluster + standalone specs |
| Regression invariants | docs/knowledge/operations.md ids 6, 9, 10, 11, 12, 13 |
Monitoring & Observability¶
Resource utilization and operator metrics:¶
- The operator does not track CPU, memory or storage utilization. An earlier
internal/monitoring/package (aResourceMonitor) was never wired into any controller and has been removed. Observe utilization with Prometheus: cAdvisor / kubelet metrics and kube-state-metrics for the pods, and Neo4j's own metrics endpoint on port 2004 for the database. - Operator metrics (
internal/metrics/): reconcile counts and durations, upgrade and backup outcomes, resource-version conflicts, per-server health and Aura API traffic. The authoritative list, including which families are populated, is in Monitoring. - Capacity before apply:
kubectl neo4j preflightchecks that a Ready node can fit the requested memory before you apply a manifest (seepreflight).
Status Management:¶
- Enhanced Status Updates: Detailed cluster state tracking
- Condition Management: Comprehensive status conditions with proper transitions
- Event Recording: Structured events for debugging and monitoring
- Connection Examples: Automatic generation of connection strings
Monitoring and Live Diagnostics¶
The MonitoringSpec field (spec.monitoring) drives two distinct
responsibilities inside the cluster controller:
1. Infrastructure setup (ReconcileMonitoring):
Creates Kubernetes resources for metrics collection:
{cluster-name}-metricsService — exposes port 2004 for Prometheus scraping{cluster-name}-monitoringServiceMonitor — tells the Prometheus Operator to scrape the metrics service- Neo4j config flags (
server.metrics.prometheus.enabled=true,prometheus.io/*annotations)
Runs on every reconcile regardless of cluster phase.
2. Live diagnostics (CollectDiagnostics):
Runs SHOW SERVERS and SHOW DATABASES via the Bolt client when the cluster is Ready:
- Writes results to
status.diagnostics(ClusterDiagnosticsStatus) - Sets
ServersHealthycondition (Truewhen all servers arestate=Enabledandhealth=Available) - Sets
DatabasesHealthycondition (Truewhen all user databases havestatus=online; thesystemdatabase is excluded) - Updates
neo4j_operator_server_healthPrometheus gauge per server (labels:cluster_name,namespace,server_name,server_address) - Non-fatal: collection errors are surfaced in
status.diagnostics.collectionErrorand the conditions are set toUnknownwith reasonDiagnosticsUnavailable
The diagnostics Bolt client is created fresh per-reconcile and closed with defer. It
never shares state with the cluster formation or upgrade clients.
Architecture invariant: All status writes in CollectDiagnostics use
retry.RetryOnConflict to handle concurrent updates without panicking.
Condition constants (defined in internal/controller/conditions.go):
ConditionTypeServersHealthy = "ServersHealthy"ConditionTypeDatabasesHealthy = "DatabasesHealthy"- Reason values:
AllServersHealthy,ServerDegraded,AllDatabasesOnline,DatabaseOffline,DiagnosticsUnavailable
Integration Architecture¶
External System Integration:¶
- Cert-Manager: TLS certificate lifecycle management
- Prometheus: Metrics collection and alerting
- External Secrets: Secret management integration
- Storage Classes: Persistent volume provisioning
- Cloud Providers: AWS, GCP, Azure LoadBalancer optimizations
Kubernetes Integration:¶
- Network Policies: Pod-to-pod communication security
- Service Mesh: Istio/Linkerd compatibility
- Ingress Controllers: External traffic routing with connection examples
- Node Affinity: Topology spread and anti-affinity rules
Testing Architecture¶
Test Strategy:¶
- Unit Tests: Controller logic and helper functions (the
internal/controllersuite runs against envtest) - Integration Tests: Full workflow testing against a real Kind cluster with the operator deployed (
core/extendedtiers — see Testing); there is no separate end-to-end or performance suite - Release verification: a manual + LLM walk on a clean cluster (Release Verification)
Test Infrastructure:¶
- Ginkgo/Gomega: BDD-style testing framework
- Envtest: Kubernetes API server for the unit-level controller suite (not used by the integration suite)
- Kind Clusters: Development and test cluster automation
- Test Cleanup: Automatic finalizer removal and namespace cleanup
Migration & Compatibility¶
Legacy Architecture Migration:¶
- Backward Compatibility: Existing clusters continue to work
- Gradual Migration: No breaking changes for existing deployments
- Resource Name Updates: New deployments use server-based naming
- Configuration Migration: Automatic handling of deprecated settings
Future Extensibility:¶
- Plugin System: Neo4j plugin management framework
- Custom Metrics: Extensible monitoring capabilities
- Event Handling: Pluggable event system for custom integrations
- Multi-Architecture: Support for different deployment patterns
Development Best Practices¶
Code Organization:¶
- Controller Pattern: Standard Kubernetes controller pattern
- Builder Pattern: Resource builders for clean separation
- Validation Framework: Centralized validation with clear error messages
- Testing Strategy: Comprehensive test coverage with multiple levels
Performance Considerations:¶
- Memory Usage: Optimized for large-scale deployments
- API Efficiency: Minimal API calls with intelligent caching
- Resource Creation: Parallel resource creation where possible
- Error Handling: Graceful error handling with proper recovery
This architecture provides a solid foundation for managing Neo4j Enterprise deployments in Kubernetes with high performance, reliability, and operational simplicity.