Resilient multi-node K/V storage for high availability, based on the Holepunch / Hypercore stack.
Replicore is an app running on remote servers to form a dynamically extensible network of nodes self-governed by Raft leadership elections.
The nodes may have three roles:
- Notary: is the primary node by election, the only one who can write
- Witness: eligible as candidate for next elections, read-only and transparent proxy to the Notary on writes
- Archivist: needs authorization to become a Witness and provides read-only access.
Replicore implements a small multi-node service with these properties:
- The data is stored as append-only feed that that can reconcile after network splits.
- The K/V database is a derived view im Hyperbee from a Hypercore feed of signed entries.
- Witness nodes replicate feeds, qualify join requests, serve reads, forward writes to the Notary.
- Writes are considered successful only after a Notary write and one Witness acknowledges.
The must be minimum two nodes for functioning.
Replicore is intended for non-Byzantine production use only within the boundaries stated in this README.
- Ease of use.
- Post-Quantum resistant join protocol.
- Noise protocol for internal node communication.
- Config-less DHT based networking setup.
- Signed K/V operations and signed heartbeat records
- Quorum based election, Raft like
- Witness to Notary write forwarding over Hypercore extensions
- Derived Hyperbee current view and history view
- Static bearer-token ACLs by keyspace
- HTTP routes for CRUD and status
- Snapshot export/import for current-state restore
- Admin snapshot export/import endpoints and CLI
- Static writer revocation on restart/reload config
- Keyring-based value encryption with live active-key rotation
- Config-driven node launcher and local bootstrap helper
- Real production auth such as JWT issuer integration, mTLS, or signed HTTP requests
- Node identity rotation
- Log pruning and feed rotation
- Backup archive lifecycle
- Production deployment packaging
- Shared-secret compromise rotation and automatic cluster-wide recovery
- Byzantine fault tolerance
npm install
Copy the environment template and fill in your values:
cp .env.example .env
Only CLUSTER_SECRET (64-char hex) is required. Generate with openssl rand -hex 32.
The entrypoint derives everything else — clusterId, identitySeed, encryptionKey,
and the DHT topic — using the same purpose-scoped argon2id KDF as the core library.
All nodes with the same CLUSTER_SECRET derive identical keys (except identitySeed,
which is unique per machine via /etc/machine-id).
Start with Caddy reverse proxy (auto-TLS for a real domain):
docker compose up -d
The node exposes its HTTP API internally on port 3000. Caddy proxies
${DOMAIN}:443 → replicore:3000. A fixed UDP port (default 49737) is
published for DHT inbound traffic. /etc/machine-id is mounted read-only from
the host to derive the cluster-scoped machine identity.
The entrypoint prints the derived DHT topic (base58) so you can verify it matches between nodes.
All runtime fields can be set via environment variables. See .env.example for
the full list.
Start a local HyperDHT bootstrap node:
npm run start:bootstrap
For a fresh cluster, bootstrap one explicit initializer:
npm run start:node -- examples/local/init-node.json
npm run start:node -- examples/local/init-joiner.json
initCluster: true is the only supported secret-first voter bootstrap path.
Another node with the same clusterSecret must join as a learner instead of
implicitly creating a second voter cluster.
Start three swarm nodes in separate terminals:
npm run start:node -- examples/local/node-1.json
npm run start:node -- examples/local/node-2.json
npm run start:node -- examples/local/node-3.json
Those three files are the current bootstrap-voter example. They still use
compatibilityMode: "legacy-static-membership" to show a pre-expanded local
cluster. The explicit fresh-cluster bootstrap path is examples/local/init-node.json.
Start a fourth node and let it join as a learner without editing the existing voter configs:
npm run start:node -- examples/local/joiner.json
Each node prints a node-ready JSON object with:
nodeIdfeedKeydataDir- HTTP bind address
- currently observed leader
clusterSecret is the shared discovery and admission root. Replicore derives:
- the Holepunch topic from
clusterSecret + clusterId - a cluster-scoped
machineIdfrom keyed Argon2d overclusterSecret + machineIdentity - the Noise transport key from
clusterSecret + machineId - the join-signing key from
clusterSecret + machineId
Committed voter authority does not come from the secret alone. A joining node starts as a learner and only becomes a voter after committed membership promotion.
Start the local setup mode without joining a cluster:
npm run start:setup -- .\data\node-1.json
Setup mode prints a setup-ready JSON object with:
- local setup URL
- requested config path
- sibling draft path
- whether the final config file already exists
- HTTP bind address
The setup server exposes a local web wizard and setup-only JSON routes such as:
GET /setup/stateGET /setup/interfacesPOST /setup/derive-machine-idGET /setup/draftPOST /setup/draft
- Setup mode binds to
127.0.0.1by default. - Do not expose setup mode on a public interface.
- The cluster secret and raw machine identity are sensitive inputs.
- The setup draft file contains sensitive material and should stay on local operator-controlled storage.
- Production use should treat setup mode as local console access, or expose it only through an operator-controlled tunnel with equivalent trust.
Write:
curl -X PUT "http://127.0.0.1:3001/kv/hash:abc?keyspace=default" `
-H "authorization: Bearer writer" `
-H "content-type: application/json" `
-d "{\"value\":{\"hello\":\"world\"}}"
Read:
curl "http://127.0.0.1:3002/kv/hash:abc?keyspace=default" `
-H "authorization: Bearer reader"
Delete:
curl -X DELETE "http://127.0.0.1:3003/kv/hash:abc?keyspace=default" `
-H "authorization: Bearer writer"
History:
curl "http://127.0.0.1:3001/kv/hash:abc/history?keyspace=default" `
-H "authorization: Bearer reader"
Replication:
curl "http://127.0.0.1:3001/status/replication"
Writers:
curl "http://127.0.0.1:3001/status/writers"
Leader:
curl "http://127.0.0.1:3001/status/leader"
Export a snapshot from a live node:
npm run snapshot -- export http://127.0.0.1:3001 admin .\tmp\snapshot.json
Import a snapshot into a live node:
npm run snapshot -- import http://127.0.0.1:3001 admin .\tmp\snapshot.json
Rotate the active encryption key on a live node:
curl -X POST "http://127.0.0.1:3001/admin/encryption/rotate" `
-H "authorization: Bearer admin" `
-H "content-type: application/json" `
-d "{\"keyId\":\"next\"}"
The example configs include an admin token for these routes.
Example configs live in examples/local/README.md and examples/local/node-{1,2,3}.json.
The node launcher expects:
dataDirclusterIdclusterSecretidentitySeed- optional
machineIdentity(or legacymachineId) - either:
initCluster: truefor the first secret-first voter in a brand-new clustercompatibilityMode: "legacy-static-membership"plusauthorizedNodeSeedsorauthorizedNodesfor bootstrap votersrole: "learner"with no static membership fields for a secret-first joining node
- either
encryptionKeyorencryption - optional
revokedNodeIds bootstraphttpauth
identitySeed and authorizedNodeSeeds are hex-encoded 32-byte seeds. The
launcher derives stable Replicore signing identities and feed keys from them.
machineIdentity is the local machine-specific input used to derive the
cluster-scoped transport machineId, Noise key, and join key.
Legacy static membership remains supported for the initial voter set while the
project keeps a local-demo path for pre-expanded clusters. Fresh clusters should
prefer the explicit initCluster: true bootstrap plus learner join and
promotion. Joining nodes should use the secret-first learner config shown in
examples/local/joiner.json.
Replicore binds each dataDir to one cluster secret hash, one local signing
identity, and one bootstrap mode. Restarting a populated directory with a
different secret, different node identity, or initCluster: true after that
directory already joined another cluster fails closed.
Simple encryption config:
{
"encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}Keyring config with rotation support:
{
"encryption": {
"currentKeyId": "primary",
"keys": {
"primary": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"next": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
}
}Revoked writers remain part of the explicit cluster membership record, but nodes stop replicating from them and reject new operations signed by them after restart with updated config.
If you already have static configs:
- keep
compatibilityMode: "legacy-static-membership"for the existing voter set - replace
topicSaltwithclusterSecretif needed - rename
machineIdtomachineIdentitywhen convenient; the old field still loads - use
initCluster: trueonly for a brand-new single-voter cluster - use a learner config like
examples/local/joiner.jsonfor new joining nodes instead of editing every existing node config by hand
bin/run-bootstrap.js: local HyperDHT bootstrap helperbin/run-node.js: config-driven node runnersrc/node.js: swarm node, replication, forwarding, leadership, durabilitysrc/http-server.js: minimal authorized HTTP surfacesrc/materialized-view.js: derived Hyperbee statesrc/operation.js: operation creation, signing, validationsrc/config-loader.js: JSON config loadingtest: integration and config tests
npm test
Longer local reliability pass:
npm run test:reliability
Example bounded reliability profile:
$env:REPLICORE_TEST_ROUNDS=2
$env:REPLICORE_TEST_TIMEOUT_MS=180000
$env:REPLICORE_TEST_PATTERN="offline leader|isolated leader|isolated follower|concurrent writes|bootstrap outage|restarted follower stays disconnected|follower write forwarding|deterministic churn"
npm run test:reliability
The tests currently cover:
- replication and restart recovery
- leader failover and follower forwarding
- single-node isolation and connected-side continuation
- stale reads on isolated followers until rejoin
- HTTP CRUD and status routes
- operation validation
- snapshot restore
- config loading
The current test suite verifies these non-malicious failure behaviors:
- follower crashes and restarts catch up from replicated feeds
- former leaders can disappear, a new leader can write, and the old leader can rejoin and catch up
- new same-secret nodes join as learners first and only become voters after committed promotion
- wrong-secret nodes do not discover, mirror, or join the cluster
- a single isolated node can continue serving local reads but cannot make durable writes
- only one connected side with the live leader and quorum can continue writing
- isolated followers can serve stale reads until they heal and catch up
- already-connected peers continue writing after bootstrap disappears
- if a follower restarts while bootstrap remains unavailable, it starts locally but does not rediscover peers from scratch, stays disconnected, and does not catch up to writes made while it was away
- learner promotion, voter removal, and node replacement go through committed membership changes
- divergent membership views remain observable through membership fingerprints, and degraded writes may stay blocked until configs converge again
- writes may fail transiently during failover windows while leader view and reachability converge
- snapshot restore and persisted data directories recover current state after severe outage or full restart
Replicore currently guarantees these properties for non-malicious failures:
- committed writes are crash-safe across restart and leader replacement
- only one partition may accept durable writes at a time; minority or split-fenced sides reject writes
clusterSecretgates discovery and first admission, but voter authority still requires committed membership- new nodes join as read-only learners, catch up, and can later be promoted to voters through quorum commit
- stale reads are explicit in runtime status when a node is isolated or split-fenced
- recovery is explicit: healed nodes reconcile to the committed leader log, and snapshot restore validates integrity metadata before import
Operators must still treat these as hard limits:
- no Byzantine or malicious-node tolerance
- compromise of the shared secret requires manual rotation and operational recovery
- clients that need freshest reads must use a leader-connected path or otherwise require a strong-read policy above the current API
- do not initialize two independent clusters with the same
clusterSecret; a fresh cluster must use one explicitinitCluster: truevoter and all other same-secret nodes must join as learners