Configuration Reference
A node reads a single TOML file, config.toml, from its home directory
(written by merod init and re-read on merod run). This page documents every
section, key, type, default, and meaning, grounded in the structs the node
actually deserializes.
Top-level keys
Section titled “Top-level keys”These live at the root of the file, outside any section.
| Key | Type | Default | Meaning |
|---|---|---|---|
mode |
"standard" | "readonly" |
"standard" |
Node role. readonly disables JSON-RPC execution and is used for observer / TEE nodes. (The values serialize lowercase; the merod init --mode flag spells the read-only value read-only.) |
[identity]
Section titled “[identity]”The libp2p transport identity for the node. Generated automatically on
merod init; you normally never hand-edit it.
| Key | Type | Default | Meaning |
|---|---|---|---|
peer_id |
string (base58) | generated | Libp2p peer ID derived from the keypair. Must match keypair. |
keypair |
string (base58) | generated | Base58-encoded protobuf libp2p keypair (the node’s private key). |
[identity]peer_id = "12D3KooWQrRJCnybfvLGZ7d4iaqGkGHpjLAB7PG6sFtXrHbkPMFc"keypair = "23jhTbrf...redacted...CyLA"[swarm]
Section titled “[swarm]”libp2p listen addresses for peer-to-peer traffic.
| Key | Type | Default | Meaning |
|---|---|---|---|
listen |
array of multiaddr | ["/ip4/0.0.0.0/tcp/2428", "/ip4/0.0.0.0/udp/2428/quic-v1", "/ip6/::/tcp/2428", "/ip6/::/udp/2428/quic-v1"] |
Addresses the swarm binds for inbound peer connections. merod init seeds TCP and QUIC on both IPv4 (0.0.0.0) and IPv6 (::). Default port is 2428. |
[server]
Section titled “[server]”The local HTTP server: admin API, JSON-RPC, WebSocket, and SSE. Default port is
2528.
| Key | Type | Default | Meaning |
|---|---|---|---|
listen |
array of multiaddr | ["/ip4/127.0.0.1/tcp/2528", "/ip6/::1/tcp/2528"] |
Addresses the HTTP server binds. merod init seeds both IPv4 (127.0.0.1) and IPv6 (::1) loopback. |
auth_mode |
"proxy" | "embedded" |
"proxy" |
How auth is enforced. proxy expects an external auth proxy in front of the node; embedded runs the bundled auth service (see [server.embedded_auth]). |
[server.admin]
Section titled “[server.admin]”| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | true |
Enables the admin API (and admin dashboard UI). When false, none of the admin routes are mounted. |
[server.jsonrpc]
Section titled “[server.jsonrpc]”| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | true |
Enables the JSON-RPC endpoint at /jsonrpc. |
[server.websocket]
Section titled “[server.websocket]”| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | true |
Enables the WebSocket endpoint at /ws. |
ping_interval_secs |
integer (seconds) | 30 |
Interval between server-initiated pings. 0 disables server pings (rely on client pings). |
pong_timeout_secs |
integer (seconds) | 10 |
If no pong arrives within this window after a ping, the connection is closed. |
[server.sse]
Section titled “[server.sse]”| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | true |
Enables the Server-Sent Events endpoint at /sse. |
[server.sealed]
Section titled “[server.sealed]”Sealed transport: traffic encrypted end to end to a TEE node’s attested key (see sealed transport). The section is left out of a generated config while it holds the default.
| Key | Type | Default | Meaning |
|---|---|---|---|
required |
bool | false |
Refuse every unsealed request with 403 sealed_required, except GET /admin-api/health, GET /admin-api/ready, GET /admin-api/tee/info and POST /admin-api/tee/attest. Turn it on for a TEE node behind a proxy you do not trust. |
With auth_mode = "proxy", a sealed request can reach only the routes merod
serves without a credential (delegated execution among them, with
delegated_access), since the proxy cannot check the route inside an envelope;
see behind an auth proxy.
[server.embedded_auth]
Section titled “[server.embedded_auth]”Present only when auth_mode = "embedded". Carries the bundled auth service
configuration (storage backend, etc.).
[bootstrap]
Section titled “[bootstrap]”| Key | Type | Default | Meaning |
|---|---|---|---|
nodes |
array of multiaddr | [] |
Bootstrap peers to dial on startup for initial network entry. merod init --network can seed this with IPFS or Calimero dev boot nodes. |
[discovery]
Section titled “[discovery]”Peer discovery and NAT-traversal behavior.
| Key | Type | Default | Meaning |
|---|---|---|---|
mdns |
bool | false |
Enables local-network mDNS peer discovery. merod init writes false: a node that announces itself and dials whoever answers is a local-development convenience, and on a shared network it is a tenancy question. Turn it on for LAN or offline development with merod init --mdns. (A config file that omits the key keeps true, so upgrading an existing node changes nothing.) |
advertise_address |
bool | false |
Whether to advertise external addresses to peers. Gates external_address. |
external_address |
array of multiaddr | [] |
Operator-supplied external addresses, seeded into the swarm’s confirmed external-address set (used for static-IP / hosted deployments instead of AutoNAT discovery). |
[discovery.rendezvous]
Section titled “[discovery.rendezvous]”| Key | Type | Default | Meaning |
|---|---|---|---|
namespace |
string | "/calimero/devnet/global" |
Rendezvous namespace used for peer registration/discovery. |
discovery_rpm |
float | 0.5 |
Discovery queries per minute (throttle floor; 0.5 ≈ one query per 120s per peer in steady state). |
discovery_interval |
duration table | { secs = 15, nanos = 0 } |
Interval between rendezvous discovery ticks. Serialized as a nested [discovery.rendezvous.discovery_interval] table with secs / nanos. |
registrations_limit |
integer | 3 |
Max rendezvous registrations to hold. |
[discovery.relay]
Section titled “[discovery.relay]”| Key | Type | Default | Meaning |
|---|---|---|---|
registrations_limit |
integer | 3 |
Max relay reservations to hold. |
[discovery.autonat]
Section titled “[discovery.autonat]”| Key | Type | Default | Meaning |
|---|---|---|---|
max_candidates |
integer | 5 |
Max AutoNAT server candidates probed for external-address confirmation. |
probe_interval |
duration table | { secs = 10, nanos = 0 } |
Interval between AutoNAT probes. Serialized as a nested table with secs / nanos. |
[sync]
Section titled “[sync]”State-sync timing. Durations are expressed in milliseconds via the _ms
suffixed keys.
| Key | Type | Default | Meaning |
|---|---|---|---|
timeout_ms |
integer (ms) | 30000 |
Per-request sync timeout. |
session_deadline_ms |
integer (ms) | 30000 (falls back to timeout_ms) |
Outer deadline for one sync session run. Optional; lower it to fail-fast on stuck sessions when not doing cold-start snapshot syncs. |
interval_ms |
integer (ms) | 5000 |
Interval between sync attempts. |
frequency_ms |
integer (ms) | 10000 |
Sync scheduling frequency. |
[datastore]
Section titled “[datastore]”| Key | Type | Default | Meaning |
|---|---|---|---|
path |
path | "data" |
RocksDB datastore directory, relative to the node home. |
[blobstore]
Section titled “[blobstore]”| Key | Type | Default | Meaning |
|---|---|---|---|
path |
path | "blobs" |
On-disk blob storage directory, relative to the node home. |
[context]
Section titled “[context]”Context configuration.
| Key | Type | Default | Meaning |
|---|---|---|---|
migration_v2 |
bool | true |
Master switch for the hybrid zero-downtime migration framework. Pin false to restore the legacy namespace-cascade write-freeze. |
[runtime]
Section titled “[runtime]”Operator-tunable WASM VM limits. Every key is optional; an absent [runtime]
section (or any absent sub-key) keeps the built-in default behavior.
[runtime.limits]
Section titled “[runtime.limits]”| Key | Type | Default | Meaning |
|---|---|---|---|
max_logs |
integer | built-in VMLimits default |
Max log entries one execution may emit. Capped at 1_000_000; exceeding the limit traps the execution. |
max_log_size |
integer (bytes) | built-in VMLimits default |
Max size of a single log line. Capped at 10 MiB; an over-long line traps the execution. |
[dag_compaction]
Section titled “[dag_compaction]”Bounds on-disk delta-log growth by pruning history older than a recent retain
window. Enabled by default; set enabled = false to opt out.
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | true |
Whether periodic DAG compaction runs. |
min_deltas_before_compact |
integer | 10000 |
Minimum delta rows a context must hold before it is eligible for compaction. |
retain_recent_count |
integer | 1000 |
Number of most-recent deltas retained after a sweep. Must be strictly below min_deltas_before_compact. |
check_interval |
duration (seconds) | 3600 |
Interval between compaction sweeps. Serialized as a plain integer count of seconds (e.g. check_interval = 3600), not a { secs, nanos } table. Must be non-zero. |
Tuning compaction for a high-write node
Section titled “Tuning compaction for a high-write node”The defaults keep ~1000 recent deltas per context and only start pruning once a context holds 10000 — fine for a quiet node, but a busy data-plane context can accumulate that history quickly. A production excerpt that prunes more aggressively on a high-write deployment:
[dag_compaction]enabled = true# Start pruning sooner on a high-write context...min_deltas_before_compact = 4000# ...but keep a generous recent window so peers can still catch up via deltas# instead of falling back to a full snapshot.retain_recent_count = 1500# Sweep more often than the 1h default so growth is bounded between checks.check_interval = 900Keep retain_recent_count comfortably above the largest delta gap a lagging
peer is expected to close incrementally: prune below that and the peer can only
reconverge via a full snapshot. retain_recent_count must stay strictly below
min_deltas_before_compact, and check_interval must be non-zero — the node
refuses to start with [dag_compaction] enabled = true and either invariant
violated, rather than silently letting the delta log grow unbounded.
Background tombstone GC (not configurable)
Section titled “Background tombstone GC (not configurable)”DAG compaction is one of two independent background maintenance loops; the other is the tombstone garbage collector, and the two reclaim different things:
| Loop | Reclaims | Cadence | Operator-tunable? |
|---|---|---|---|
| DAG compaction | old delta-log history older than the recent retain window | [dag_compaction] check_interval (default 1h) |
Yes — the [dag_compaction] keys above. |
| Tombstone GC | expired CRDT tombstones (the markers a deleted entity leaves behind) | fixed 12h | No — not a config.toml key. |
When an entity is deleted, the CRDT layer keeps a tombstone so the deletion
can win against a concurrent or out-of-order write that arrives later. A
tombstone is retained for a fixed 24h before it becomes eligible for
collection, and the GC loop runs every 12h, scanning each context and
deleting tombstones older than that window. Both durations are compile-time
constants, not config keys — there is no [gc] section and no gc_interval
knob, so nothing here needs (or accepts) tuning. The retention window is
deliberately longer than the GC interval, and shorter than the offline window
that forces a full resync, so a node that reconnects within the window can still
reconcile deletions incrementally.
[registry]
Section titled “[registry]”Where this node gets application bundles from - and there is exactly one such place. The node has no second route behind it, so this section decides both what it can install and whether it serves application bytecode to peers at all.
| Key | Type | Default | Meaning |
|---|---|---|---|
mode |
"http" | "dht" |
"http" |
The node’s one application source. http fetches from base_url; dht fetches from context members over blob share. |
base_url |
URL | written by merod init as "https://apps.calimero.network/" |
Base of the registry http mode resolves versions against. Override at init with merod init --registry-url <URL>. Unused in dht mode. |
An absent [registry] section means mode = "http" with no base_url, which is a node
that cannot resolve applications at all - see the failure modes below.
merod init writes the public registry into a fresh config.toml, so a node initialized
normally starts out resolvable; a mirror or an air-gapped deployment changes that one line.
[registry]mode = "http"base_url = "https://apps.calimero.network/"What each mode does
Section titled “What each mode does”http |
dht |
|
|---|---|---|
| Fetches applications from | base_url, by package/version |
context members, authorized by membership |
| Serves application bytecode to peers | no - neither announced nor served | yes |
Installs package@version by name |
yes | no - governance or a local .mpk path only |
| Serves user-data blobs | yes | yes |
The serving gate is NodeClient::may_share_blob.
An http node is not a source of application bytecode, so it withholds exactly that: an
application’s bytecode and compiled artifacts, including every named service’s.
User-data blob sharing is untouched in both modes.
dht mode is for deployments with no registry reachable from the nodes - a closed fleet, a
test harness, a lab network - where members hand each other the bytes governance named.
It cannot bare-install by name, because a peer fetch needs a context to authorize against.
Environment overrides
Section titled “Environment overrides”Two variables override the file, applied when merod run loads the config:
| Variable | Overrides |
|---|---|
CALIMERO_REGISTRY_MODE |
mode (http or dht) |
CALIMERO_REGISTRY_URL |
base_url |
A malformed value is a startup error, not a silent fall back to the file’s setting: a misconfigured node must not start.
How a version is resolved
Section titled “How a version is resolved”The node appends the coordinates carried by the governance op to base_url -
{base_url}/artifacts/{package}/{version}/{package}-{version}.mpk - and accepts the
download only if its blob id equals the bytecode_id the group named, so a wrong registry
can only cause a fetch failure, never a code substitution.
Because the value is operator-chosen, no host guard applies to it, which is what makes a
private or air-gapped registry reachable. It is also the only URL the node ever fetches an
application from - nothing remote-chosen names a location.
See how bytecode reaches a node for
the full picture.
When it is misconfigured
Section titled “When it is misconfigured”There is no fallback, so a misconfiguration surfaces rather than degrading:
| Situation | What you see |
|---|---|
mode = "http", no base_url, installing by coordinates |
the install fails with [registry] mode = "http" needs a base_url to fetch from |
mode = "http", no base_url, a group names bytecode |
no application source is configured in the log; the context keeps its current version and retries on next access |
base_url points somewhere with nothing published there |
the install reports the coordinates as unpublished (retryable - the version simply is not there yet) |
base_url points at a registry that repacks bundles |
every fetch fails a blob id mismatch and no node moves version |
Trusted Execution Environment configuration. The entire section is optional and absent on non-TEE deployments.
When [tee] is present, merod run fetches the datastore’s encryption key from the
KMS and refuses to start without it. Create a TEE node with merod init --kms-url <URL> rather than adding this section afterwards. init is what writes the node’s
signing identity and account root, so a store encrypted only from the first run
would already hold both in plaintext. Having been written unencrypted, that store also
could not be opened encrypted at all, because every read is decrypted. --kms-url
fetches the key before anything is written and saves [tee.kms] with that URL.
The KMS is verified against the signed mero-tee release policy, so init --kms-url
requires the release to be named in MERO_TEE_VERSION (or MERO_KMS_VERSION /
MERO_KMS_RELEASE_TAG) and refuses otherwise. A store encrypted with a key from an
unverified KMS would be readable by whoever runs that endpoint. In a build without
mock-attestation, merod run and merod kms probe hold to the same rule: they refuse a
[tee.kms] with no named release and no enabled config allowlists (see enabled
below), before sending the KMS any request.
[tee.kms]
Section titled “[tee.kms]”| Key | Type | Default | Meaning |
|---|---|---|---|
url |
URL | — (required if present) | URL of the mero-kms service (the KMS cluster for this release). Its host may be a DNS name resolving to several replicas; merod gives each resolved address a share of a 10s connect timeout (~2s each with five) and moves on to the next, so a dead replica still in the record does not stall key fetch. |
[tee.kms.tls]
Section titled “[tee.kms.tls]”| Key | Type | Default | Meaning |
|---|---|---|---|
ca_cert_path |
path | unset | PEM CA certificate added to the KMS TLS trust store. Absolute path to an existing file. |
client_cert_path |
path | unset | PEM client certificate for mTLS. Must be set together with client_key_path. |
client_key_path |
path | unset | PEM client private key for mTLS. Must be set together with client_cert_path. |
[tee.kms.attestation]
Section titled “[tee.kms.attestation]”Policy for verifying KMS self-attestation before requesting storage keys.
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | false |
Enable KMS attestation verification before requesting keys. In a build without mock-attestation, run and kms probe refuse a KMS with no named release unless this is true, accept_mock is false and the allowlists below are set. |
accept_mock |
bool | false |
Accept mock quotes (development only). Bypasses real attestation guarantees. |
allowed_tcb_statuses |
array of string | ["UpToDate"] |
Allowed TCB statuses for quote verification. Required non-empty when enabled and not accept_mock. |
allowed_mrtd |
array of string (hex) | [] |
Allowed KMS MRTD measurement values. Required non-empty when enforcing real attestation. |
allowed_rtmr0 |
array of string (hex) | [] |
Allowed KMS RTMR0 values. Required when enabled and not accept_mock. |
allowed_rtmr1 |
array of string (hex) | [] |
Allowed KMS RTMR1 values. Required when enabled and not accept_mock. |
allowed_rtmr2 |
array of string (hex) | [] |
Allowed KMS RTMR2 values. Required when enabled and not accept_mock. |
allowed_rtmr3 |
array of string (hex) | [] |
Allowed KMS RTMR3 values. Required when enabled and not accept_mock. |
binding_b64 |
string (base64) | unset | Optional 32-byte binding value for the /attest call. Defaults to the domain-separator binding, SHA-256("mero-kms-attest-v1"). |
policy_json_path |
path | unset | Path to an externally-generated attestation policy JSON (for startup scripts that fetch/verify signed policy artifacts). |
Representative config.toml
Section titled “Representative config.toml”A complete, typical single-node config (chainless local self-signer):
mode = "standard"
[identity]peer_id = "12D3KooWQrRJCnybfvLGZ7d4iaqGkGHpjLAB7PG6sFtXrHbkPMFc"keypair = "23jhTbrf...redacted...CyLA"
[swarm]listen = [ "/ip4/0.0.0.0/tcp/2428", "/ip4/0.0.0.0/udp/2428/quic-v1", "/ip6/::/tcp/2428", "/ip6/::/udp/2428/quic-v1",]
[server]listen = ["/ip4/127.0.0.1/tcp/2528", "/ip6/::1/tcp/2528"]auth_mode = "proxy"
[server.admin]enabled = true
[server.jsonrpc]enabled = true
[server.websocket]enabled = trueping_interval_secs = 30pong_timeout_secs = 10
[server.sse]enabled = true
[bootstrap]nodes = []
[discovery]mdns = trueadvertise_address = false
[discovery.rendezvous]namespace = "/calimero/devnet/global"discovery_rpm = 0.5registrations_limit = 3
[discovery.rendezvous.discovery_interval]secs = 15nanos = 0
[discovery.relay]registrations_limit = 3
[discovery.autonat]max_candidates = 5
[discovery.autonat.probe_interval]secs = 10nanos = 0
[sync]timeout_ms = 30000session_deadline_ms = 30000interval_ms = 5000frequency_ms = 10000
[datastore]path = "data"
[blobstore]path = "blobs"
[context]migration_v2 = true
[runtime.limits]max_logs = 2048max_log_size = 32768
[dag_compaction]enabled = truemin_deltas_before_compact = 10000retain_recent_count = 1000
[registry]base_url = "https://apps.calimero.network/"