SSyncropel Docs

Federation

How two or more Syncropel instances share records while keeping trust boundaries intact — the model, the pieces, and the choices behind them.

What federation is

Federation is how two or more Syncropel instances — your laptop, a shared server, a teammate's laptop, a hosted service — share records without any of them becoming a single point of failure.

If you've only ever run a single spl serve instance, federation is optional. You get all the records, threads, trust, and task tracking without it. If you want more than one instance — for collaboration, for redundancy, for the hybrid hosted+local pattern — federation is the layer that ties them together.

The mental model

Three ideas shape how federation works in Syncropel. Everything else follows from these.

1. Records are immutable and content-addressed

Every record has a content hash derived from its canonical JSON form. The same record on your laptop and on a teammate's server has the same hash. You can't edit a record — only emit new ones that reference it. This turns sync into a much simpler problem: do these two instances have the same set of hashes on this thread?

2. Federation is pull-based

Each instance decides what to fetch, from which peer, at what cadence. Nobody pushes into you uninvited. The transport is pull-per-direction: the source responds to GET /v1/sync/changes?since=<cursor> queries; the receiver decides when to ask. A pair between two stewards establishes the credentials and consent envelope; both sides then pull from each other on their own schedules.

This choice is deliberate. Push-based federation requires the source to know its subscribers; pull-based means each instance is responsible for its own state.

Every record is signed by the actor who emitted it. The signature is verified on ingest using the actor's Decentralized Identifier (DID). Cross-namespace record sharing requires an explicit consent grant — without it, records stay in their namespace even when peers are paired.

This means: a peer finding you on the network doesn't grant them any access. Pairing with a peer doesn't leak records across namespaces. The security model is layered; discovery and pairing are separate from authorization.

The pieces

Identity — who a peer is

Each Syncropel instance has an identity: an Ed25519 keypair and a DID. Three DID methods are supported, each suiting a different audience:

  • did:key — the DID is the public key itself. Zero infrastructure, zero signup. Generated by spl init in under a second. Ideal for first-time users, ephemeral agents, or anyone who doesn't need a human-readable identifier.
  • did:web — the DID points to a domain you control (e.g., did:web:alice.dev). Resolution is a standard HTTPS GET to https://alice.dev/.well-known/did.json. No directory service required; if you own a domain, you own your identity.
  • did:sync — the DID points to a directory service that stores the public key and service endpoint. Supports key rotation and handle lookup. The default directory is operated by Syncropic; you can also self-host.

All three methods produce the same downstream primitive: a public key that verifies signatures, plus an endpoint that serves the Syncropel API. The difference is in how the DID is resolved and whether it supports rotation.

Pairs — how instances subscribe to each other

A pair is a record-bound persistent relationship between two instances. Pairs are first-class records on the reserved thread th_federation_pairs — they are not config, not session state, not ambient configuration. They survive snapshot and restore, they are auditable, and their state is fold-derived from the record log.

A pair carries:

  • peer_did — the peer's identity (the load-bearing handle; URLs change, DIDs persist)
  • peer_url — where to reach them on the network (cached, refreshed)
  • stateestablishing, active, paused, degraded, revoked
  • tokens — reciprocal bearer tokens minted during handshake (each side holds the other's; bound to peer DID)
  • cursors — per-thread sync position (one per thread the pair syncs)

State machine transitions are themselves records. Pause emits an update record; revoke emits a terminal revoke record. Operator-facing audit lives on a sibling thread th_audit_federation_pairs so subscribing to lifecycle events is cheap and decoupled from the main pair-state computation.

Establishing a pair is a single command: spl federation pair <peer-url>. Behind the scenes it discovers the peer manifest, runs a 3-roundtrip nonce-protected handshake, mints reciprocal service accounts + bearer tokens (90-day TTL with auto-renew at 83 days), and persists the genesis record on both sides. spl sync then consults the pair store automatically — no manual token plumbing.

Stewards as peers

Federation in Syncropel is not a fleet (intra-org coordination under one operator) and not a hub (centralized broker). It is a network of peers, where every spl serve instance can be a citizen with one command.

This matters because the hybrid hosted+local model is foundational: a user signs up at syncropel.com, gets a hosted steward in under 30 seconds, and can leave any time to run spl serve on their own laptop or server with all their records intact. That only works if every steward — hosted or local, ephemeral or long-lived — is a real peer in the same protocol. The pair primitive is what makes "real peer" mean something operationally: any two stewards can establish trust, exchange records, and dissolve the relationship via the same primitive, with no central coordinator.

Topology is emergent, not configured:

  • Pair (2-node) — the base case. One pair record on each side; transport handles the wire.
  • Star (1+N) — most common shape at small scale. One central instance pairs with N peers individually; each pair is independent.
  • Mesh (full) — practical at N<10. Above that, pair count + revocation cascades become prohibitive.
  • Hierarchical — rarely the right shape. Delegate by namespace, not by federation topology.

Operators reason about pairs, not graph shapes. The protocol does not impose a topology.

That emergent network of peers — and the cross-instance discovery it makes possible — is what Syncropel calls the Graph. The Graph is a pattern, not a server: every paired instance is already part of it.

Thread filtering on a pair

A pair connects two stewards. By default, all consent-permitted threads sync. When you want narrower control — e.g., share th_shared_workspace and th_shared_design but not the rest of default — apply per-pair filters:

  • spl federation thread-allow <pair-id> <thread-id> — allowlist a specific thread for this pair. Records on threads not in the allowlist don't leave.
  • spl federation thread-rule <pair-id> '<CEL expression>' — a CEL predicate evaluated against record and thread(). Useful for "all threads under namespace X" or "only threads tagged shared".

Filters compose with consent: a record needs to clear consent (cross-namespace permission) AND match the pair's allowlist/rules to propagate.

Legacy sync-only pairs (pre-v0.24)

Before the pair primitive landed, pair meant a unidirectional thread-scoped subscription configured via spl fleet sync add with explicit token plumbing. The CLI still supports it for backward compatibility — see the legacy section in CLI reference — but new pairs should use spl federation pair <peer-url>.

Syncropel has a 5-level namespace hierarchy for tenancy and policy composition. When federation crosses namespace boundaries — your default namespace syncing to a partner team's partner-team namespace — the consent filter decides what passes.

The default is strict: no consent grant, no records cross the boundary. Grants are records on a reserved thread th_consent and specify:

  • source namespace (where records originate)
  • target namespace (who's allowed to receive)
  • hash levels (L0 = full body, L1/L2/L3 = progressively redacted structural information)

A grant without L0 means records still arrive on the other side but their bodies are replaced with {"redacted": true} and their signatures stripped. Metadata shapes propagate; payload does not. Useful for telemetry collection, audit archives, or any case where you want presence signals without content.

Discovery — how peers find each other

You don't have to hand-configure every peer URL. Four ways to find peers (all optional, each suiting different deployments):

  • LAN broadcast (mDNS) — instances on the same local network announce themselves; other instances discover them in under a second. Magic for home or office setups.
  • did:web domain lookup — if you know a peer's domain, resolve their DID over HTTPS. Works globally, no directory required.
  • did:sync directory query — ask a directory (either the default Syncropic-hosted one at discovery.syncropel.com or a self-hosted one) for peers in a namespace. Works across networks.
  • Transitive introduction — ask a peer you're already paired with for the peers they know. Opt-in per operator.

Discovery is purely "here's a DID and where to reach them." It never automatically creates pairs. The operator always confirms before pairing.

What federation is not

Three common misconceptions worth heading off.

Federation is not a mesh overlay. There's no distributed hash table, no routing, no multi-hop delivery. Every pair is a direct HTTPS connection between two instances. Discovery helps them find each other; data flows point-to-point.

Federation is not full replication. A pair connects two stewards but does not auto-replicate every record on either side. Consent grants gate cross-namespace traffic; per-pair allowlists and CEL rules narrow within. The default isn't "replicate everything"; the default is "nothing crosses without an explicit consent grant."

Federation is not a trust mechanism. Pairing with a peer doesn't mean you trust them. Every record they send is signed, verified, consent-filtered, and trust-weighted independently. The pair says "we're willing to exchange records under these consent and filtering rules" — not "this actor's records auto-receive trust evidence."

How the pieces fit together

A typical end-to-end flow:

  1. Alice runs spl init on her laptop. A did:key identity is generated. She starts spl serve.
  2. Bob runs spl init on his workstation. Same thing — his instance is up. Both have signed manifests at /.well-known/syncropel.
  3. Alice runs spl federation pair https://bob.example/ — discovery + 3-roundtrip handshake + reciprocal token mint + persistent pair record on th_federation_pairs. Bob's side persists a mirror pair record automatically. The pair is bidirectional from this moment.
  4. (Optional) Alice narrows what she shares: spl federation thread-allow <pair-id> th_shared_workspace. Without an allowlist, all consent-permitted threads sync.
  5. Alice emits a record: spl intend --thread th_shared_workspace "review the design".
  6. Within ~5 seconds Bob's instance has pulled it, signature-verified it, run consent + thread filters, and ingested it into his local store. Alice's side pulls Bob's emissions on the same cadence.
  7. If Alice and Bob are in different namespaces, the consent filter runs. If there's no matching grant, nothing propagates. If there's a grant with L0, records arrive intact. If the grant excludes L0, records arrive projected to the granted hash level — their structure and flow without the exact contents — rather than a redaction stub. A thread that was never granted doesn't appear at all (its existence never leaks).

The whole flow is about a minute of setup. Once a pair exists, propagation is automatic, continuous, and bidirectional.

What's enforced today

Federation covers pair creation, signature verification, consent filtering, discovery (mDNS, DNS-based federation manifests, and did:web + did:sync actor identity), async relay for offline peers, and the pull loop with classified error codes. That's enough for "run two instances, pair them, share a thread" as a real workflow.

One piece is designed but not yet shipped:

  • MLS end-to-end encryption for pairs — opt-in per pair. Today, pair traffic is TLS-secured point-to-point; MLS would add forward-secrecy and post-compromise security to the record bodies themselves. Opt-in per pair; on the near-term roadmap pending upstream library stabilization.

What's enforced today is the strict default: no consent, no crossing. An operator who pairs two instances and emits records on a shared thread sees them propagate; an operator who wants to sync across namespaces without an explicit consent grant sees nothing propagate. That's the intended behaviour, and it holds.

What's next

  • The Graph — the federation pattern: how instances discover each other and the workspaces they publish
  • Universal traversal — see everything you reach across your instances as one view; how shared threads appear with exactly the granted access
  • Federation pair guide — hands-on spl federation pair walkthrough with two local instances
  • Consent management — the full consent grant flow for cross-namespace sharing
  • Records, Threads, Actors — the primitives federation composes over
  • Namespaces — the unit of tenancy + governance that consent grants operate on

On this page