Running federation
Shipped in v3-alpha (Beta). Cross-instance memory sharing with encrypted, signed payloads (Ed25519 + mTLS). Interfaces may still change before 3.0 stable.
This is the operational companion to the
federation concept page, which explains what
federation is. This page covers how to run it: enabling the feature, key
material, the mTLS boundary, peer approval, conflict strategies, and the
tombstone/erasure behavior. It follows the authoritative spec in
docs/architecture/FEDERATION.md.
Federation is not a real-time replication layer — it is an eventually-consistent knowledge sync with explicit conflict resolution. For synchronous high availability, use Postgres streaming replication instead.
Enabling federation
Federation lives in the optional @mnemosyne/federation package and is wired in
at the composition root behind a feature flag — the zero-dependency core never
imports it. When the flag is off, none of the sync machinery runs.
if (config.features.federation) {
const { FederationPeerManager } = await import("@mnemosyne/federation");
await new FederationPeerManager(mnemo, config.federation).start();
}Peers are listed explicitly in mnemo.config.yaml (static discovery is
recommended for production). Each peer entry carries an id, endpoint, the
workspaces it may exchange, a direction, and an optional certificate pin:
federation:
peers:
- id: "instance:eu.example.com"
endpoint: "https://mnemo-eu.example.com"
certPin: "sha256/Abc123..."
workspaces: ["ws_shared", "ws_products"]
direction: "bidirectional"
- id: "instance:dr.example.com"
endpoint: "https://mnemo-dr.example.com"
certPin: "sha256/Xyz789..."
workspaces: ["ws_shared"]
direction: "push" # this instance pushes; DR only receivesDNS SRV records (_mnemosyne._tcp.<domain>) are also supported for dynamic
deployments and are re-resolved every 5 minutes; discovered peers are still
subject to certificate pinning before trust is established. mDNS LAN
auto-discovery is not implemented — there is no discovery/ code in the
package.
The mTLS boundary
Mnemosyne does not terminate TLS in the application. The server runs raw
node:http; a trusted TLS-terminating proxy (nginx, a cloud load balancer, or
Fly.io’s edge) performs the mutual-TLS handshake and forwards the verified
client certificate in the x-mnemo-client-cert header. See
docs/architecture/MTLS_BOUNDARY.md for the full rationale.
The application only trusts that header when the request originates from the
proxy IP configured in MNEMO_TRUSTED_PROXY_IP. The middleware compares the
first X-Forwarded-For entry (the IP the TLS terminator saw on the wire)
against that value; if they differ, clientCert is left unset and the request
falls through to other auth (API key, Bearer token).
| Variable | Required | Description |
|---|---|---|
MNEMO_TRUSTED_PROXY_IP | Recommended | IP of the TLS-terminating proxy. When set, x-mnemo-client-cert is trusted only from this IP. When unset, the header is trusted from any source (single-node/dev behavior). |
The proxy must strip or overwrite any client-supplied
x-mnemo-client-cert before forwarding, and the Mnemosyne server must not
be directly internet-accessible — bind it to 127.0.0.1 or an internal VPC
address so all traffic flows through the trusted terminator.
On top of transport mTLS, every federation request also carries a short-lived
signed JWT (exp = iat + 300, a unique jti for replay prevention, and a
payload_hash binding the token to the request body). The jti is checked
against a Redis replay store; a repeat is rejected with 401.
Generating and registering Ed25519 peer keys
Federation supports an optional application-layer signing + sealing layer on top of transport mTLS. It is opt-in via key material: when keys are absent, serve and pull fall back to plaintext (transport security still applies), so existing deployments keep working unchanged.
On this instance (serve side)
# Ed25519 private key (PEM) — signs the served envelope
openssl genpkey -algorithm ed25519 -out fed_priv.pem
export MNEMO_FEDERATION_PRIVATE_KEY="$(cat fed_priv.pem)"
# Shared 32-byte symmetric key (base64) — seals item payloads
export MNEMO_FEDERATION_SYMMETRIC_KEY="$(openssl rand -base64 32)"
# Public key to share with peers that pull from this instance
openssl pkey -in fed_priv.pem -pubout -out fed_pub.pemWhen both env vars are set, GET /memory/federation/items returns a signed,
sealed envelope. On bad or short keys, boot logs
federation.serve_crypto_init_failed and falls back to plaintext.
On each peer (pull side)
In the local peer registry (PeerConfig), set per peer:
signingPublicKey— the peer’s Ed25519 public key (PEM), used to verify the signature on pull.symmetricKey— the shared 32-byte symmetric key (base64), used to open sealed items.
When both are present for a peer, the pull path enforces verify + open and rejects items that fail either; otherwise it accepts plaintext for back-compat. Keys are never accepted from the peer payload — only from local config.
Transport-level certificate pins rotate on roughly a 90-day cycle with a
72-hour overlap window. A pin violation terminates the connection immediately
and writes a federation_cert_pin_mismatch audit entry.
Approving peers and driving sync
Federation exposes REST endpoints under /v1/memory/federation/*. All of them
require the federation scope in the API key’s JWT claims.
| Method & path | Purpose |
|---|---|
POST /v1/memory/federation/sync | Initiate a push/pull with a peer (returns 202 + a jobId) |
GET /v1/memory/federation/sync/:jobId | Poll an async sync job’s status |
GET /v1/memory/federation/peers | List connected peers and their state |
GET /v1/memory/federation/peers/pending | List discovered-but-unapproved peers |
POST /v1/memory/federation/peers/:id/approve | Approve a pending peer |
DELETE /v1/memory/federation/peers/:id | Remove a peer |
POST /v1/memory/federation/resolve | Resolve a contested conflict |
GET /v1/memory/federation/items | Serve the signed, sealed sync envelope (serve side) |
Kicking off a sync returns a job handle that you poll to completion:
// POST /v1/memory/federation/sync → 202
{ "jobId": "job_sync_001", "status": "processing" }// GET /v1/memory/federation/sync/job_sync_001
{
"jobId": "job_sync_001",
"status": "completed",
"peer": "instance:eu.example.com",
"itemsSynced": 142,
"conflicts": 2,
"durationMs": 3210
}A peer moves through a connection lifecycle —
DISCOVERED → HANDSHAKE → ACTIVE, dropping to DEGRADED on timeout and
reconnecting with exponential backoff (30s → 2m → 10m → 30m → 1h ceiling). A
peer with no successful reconnect for 24 hours is flagged stale and raises an
admin alert.
Conflict-resolution strategies
When the same logical item (same origin id) exists on both sides with
different content hashes, the incoming version is run through a resolution
pipeline before it is written. The pipeline (FEDERATION.md §12) is, in order:
- Resurrection guard — if the id is in
mnemo_deletion_log, reject withRESURRECTION_BLOCKED(a tombstone is idempotently ACKed). This runs first, so a lagging peer can never re-introduce a deleted item. - Hash match → no-op, accept (deduplicated).
- Attribution cap + version guard — every incoming item is clamped to
federation_received(trust score 20) regardless of what the sender claims; any localuser_stated/verifiedfact can never be overwritten by a federated item; and items jumping more than 100 versions ahead are rejected. - Trust comparison — within the same tier, the higher trust anchor wins when the verdict is confident.
- Configured strategy — applied when trust is ambiguous.
The strategy is chosen per sync via SyncRequest.conflictResolution:
| Strategy | Behavior |
|---|---|
theirs_wins (LWW) | Accept incoming, close the local version’s validity |
ours_wins (LWW) | Reject incoming, keep local |
consensus | Run a weighted supermajority vote across peers; a winner needs > 66% of weighted votes, otherwise the item is contested |
manual | Mark contested immediately, skipping auto-resolution |
Consensus weighting comes from the local peer trust config (never
self-reported by a peer), decayed by clock skew and item freshness. The
implementation lives in packages/federation/src/consensus.ts
(resolveConsensus) and trust.ts (the frozen 9-tier TRUST_ANCHORS).
When resolution lands on contested, both versions are preserved: the local
one stays active, the incoming one is stored with status = "contested", a
conflict_review task is created, and both appear in recall flagged
contested: true. A workspace admin then settles it via
POST /v1/memory/federation/resolve, which accepts either the legacy
{ itemA, itemB } LWW form or a { items: FederationClaim[] } weighted
consensus form.
Secure envelopes, tombstones, and GDPR erasure
Secure envelope. Sync payloads are wrapped in a signed envelope: an Ed25519
signature over SHA-256(canonical_json(items)), with item bodies sealed under
AES-256-GCM. The helpers — buildSecureItemsEnvelope / openSecureItemsEnvelope,
signPayload / verifyPayload, sealItem / openItem — are exported from
@mnemosyne/federation (secureEnvelope.ts, crypto.ts). Verification failures
surface as FederationVerifyError / FederationDecryptError.
Tombstones. Deletes propagate as tombstone sync items (a type suffix of
:deleted, or an ERASE_NOTIFY message for GDPR erasure). On receipt, the
instance hard-deletes the item, writes a row to mnemo_deletion_log, flushes
the Redis hot tier, purges the embedding from the vector store, and — when
requiresAck is set — returns a SYNC_ACK confirming the deletion. The
deletion-log entry is what powers the resurrection guard above, so an erased
item stays erased across the mesh even if a stale peer re-offers it.
Monitoring
Federation emits Prometheus metrics on /metrics (labelled per peer):
mnemo_federation_lag_seconds, mnemo_federation_items_synced_total,
mnemo_federation_conflicts_total, mnemo_federation_peers_active /
_degraded, mnemo_federation_certificate_expiry_days, and
mnemo_federation_replay_detected_total. Recommended alerts from the spec:
| Alert | Condition | Severity |
|---|---|---|
FederationPeerDown | mnemo_federation_lag_seconds > 3600 | warning |
FederationCertExpiringSoon | mnemo_federation_certificate_expiry_days < 14 | warning |
FederationReplayAttack | rate(mnemo_federation_replay_detected_total[5m]) > 0 | critical |
See also
- Federation protocol — what federation is
- Security & isolation — the mTLS boundary in context
- Trust-anchor consensus — zero-LLM conflict resolution