Hub-and-spoke sync between Flair instances. A hub instance coordinates sync for one or more spoke instances.
Federation lets multiple Flair instances share memories, relationships, and agent records. Each instance maintains its own Ed25519 identity. Sync requests are signed and verified against pinned peer public keys.
Spoke A ──[signed sync]──▶ Hub ◀──[signed sync]── Spoke B
- Hub: accepts sync pushes from paired spokes, can relay records between peers
- Spoke: pushes local changes to the hub, receives changes from other spokes via the hub
Pairing connects a spoke to a hub with mutual key pinning and an auth-aware handshake that works across all Harper topologies, including Harper Fabric.
1. Hub admin generates a pairing token triple
On the hub machine, the admin runs flair federation token. The command emits a JSON triple containing a one-time bootstrap credential:
flair federation token --admin-pass <hub-admin-password>Output (a single JSON object):
{"token":"<one-time-pairing-token>","user":"<bootstrap-username>","password":"<bootstrap-password>","expiresAt":"<ISO-8601-timestamp>"}Tokens expire after 60 minutes by default. Use --ttl <minutes> to adjust.
2. Hub admin shares the triple with the spoke admin
The JSON triple is shared out-of-band (secure file transfer, password manager, or similar). It must be stored as a plain JSON file on the spoke side or piped via stdin.
3. Spoke admin runs the pair command
On the spoke machine:
# From a file
flair federation pair <hub-url> --token-from /path/to/triple.json
# From stdin
cat triple.json | flair federation pair <hub-url> --token-from -4. Behind the scenes
- The bootstrap user authenticates at the platform layer (works on standalone deployments and Harper Fabric alike).
- The resource handler validates the pairing token, the signed request body, and the binding between the bootstrap user and the token.
- On success the hub creates a
Peerrecord for the spoke and removes the temporary bootstrap user. The spoke records the hub as its peer.
After pairing, both instances pin each other's Ed25519 public keys and are ready to sync.
Earlier designs relied on allowCreate=true combined with body-only authentication. That approach works on single-component deployments but breaks on Harper Fabric, where the platform authentication gate fires before the resource handler sees the request. The bootstrap-user flow (Option B) makes the pair handshake auth-aware so it operates correctly on all Harper topologies: standalone, Fabric single-node, and Fabric multi-node.
When the hub runs on Harper Fabric, adapt the hub URL to the Fabric pattern:
# 1. Hub admin generates the triple (on the Fabric host)
ssh hub-host
flair federation token --admin-pass <hub-admin-password> > /tmp/pair-triple.json
# 2. Transfer the triple to the spoke admin (out-of-band)
scp hub-host:/tmp/pair-triple.json ./pair-triple.json
# 3. Spoke admin pairs using the Fabric URL
flair federation pair https://<fabric-node>:19926/<instance-name> --token-from ./pair-triple.jsonReplace <fabric-node>, <instance-name>, and <hub-admin-password> with your actual values.
Push local changes to the hub:
flair federation sync --admin-pass <password>
# Output: ✅ Synced 12 records (0 skipped) in 145msEvery federation request (pair, sync) is signed with the sender's Ed25519 private key. The receiver verifies the signature against the peer's pinned public key. Unsigned or tampered requests are rejected with 401.
The signature covers the canonical JSON of the request body (keys sorted recursively, signature field excluded).
Private key seeds are stored in ~/.flair/keys/<instanceId>.key, encrypted with AES-256-GCM. The encryption key is derived via HKDF from:
FLAIR_KEY_PASSPHRASEenvironment variable (recommended for production), or- An auto-generated random passphrase at
~/.flair/keys/.passphrase(mode 0600)
If neither the env var nor the passphrase file can be accessed, federation identity creation fails. Private keys are never stored in the database.
New peers must present a valid, unexpired, unused pairing token. This prevents unauthorized instances from joining the federation. Tokens are generated by the hub admin and are single-use.
Re-pairing an existing peer (same instance ID, same public key) does not require a token.
Spoke instances can only push records they originated. A spoke cannot overwrite records from another spoke or from the hub. The hub can relay records from any origin.
Records with updatedAt more than 5 minutes in the future are rejected. This prevents an attacker from using far-future timestamps to permanently win last-write-wins (LWW) merge conflicts.
| Command | Description |
|---|---|
flair federation status |
Show instance identity and peer connections |
flair federation pair <hub-url> --token-from <file> |
Pair this spoke with a hub using a token triple file (or - for stdin) |
flair federation sync |
Push local changes to the hub (one-shot) |
flair federation watch [--interval <s>] |
Run sync in a foreground daemon loop (default 30s) |
flair federation reachability |
Probe local instance + each paired peer (read-only) |
flair federation token [--ttl <min>] |
Generate a one-time pairing token triple (hub only) |
Federation uses record-level last-write-wins (LWW) with ISO timestamp comparison. When two instances modify the same record, the one with the later updatedAt wins. Field-level LWW is planned for a future version.
If the hub's configured port in config.yaml differs from the port the spoke is targeting, federation requests fail with a connection error. Verify the port matches between the spoke's pair URL and the hub's config.yaml (federation.port or the instance's listen port).
# On the hub, confirm the listening port
grep -E 'port|federation' ~/.flair/config.yamlWhen troubleshooting on the hub, fetching /federation/instances/<id> locally (e.g. via curl localhost) may return a 401 if the request does not carry the authentication headers the platform layer expects. On Fabric this gate is enforced even on localhost. Use the CLI tooling (flair federation status) instead of raw HTTP calls for local inspection.
If pairing fails with a role-not-found error, the hub instance may be missing the flair_pair_initiator role. This role is created automatically during flair init --remote but can be lost if the database was reset or migrated manually. Re-run flair init --remote on the hub to restore default roles, then retry pairing.
After a successful pairing, the temporary bootstrap user is automatically deleted. If it persists on a Fabric deployment, check that the hub's Harper operations log does not show a rollback or permission error during the cleanup step. Manually removing the stale bootstrap user via the Harper Studio is safe if needed — it is never used after pairing completes.
If a spoke was previously paired with a different hub (or the hub's identity key changed), the spoke may retain a stale Peer record pointing to the old hub. Remove the stale record before pairing with the new hub:
# Show current peers
flair federation status
# Remove a specific peer (replace <instanceId>)
flair federation unpin <instanceId>- HTTP push only — no persistent WebSocket connections or real-time sync
- Manual or watch-loop sync —
flair federation syncis one-shot;flair federation watch --interval 30runs it on a loop in a foreground daemon (wrap in launchd / systemd for production) - Single hub — spoke-to-spoke sync goes through the hub
- Record-level LWW — not field-level; concurrent edits to different fields of the same record may lose data