Federation Protocol
How independent Loqa instances discover, authenticate, and exchange events with each other โ while preserving end-to-end encryption from device to device.
01Protocol Overview
Loqa federation enables independently operated instances to exchange messages, membership changes, reactions, and typing indicators in real time โ without a central coordinator. The protocol is designed around four core principles.
Decentralized
No single server is authoritative. Each instance manages its own users, guilds, and keys. Peers discover and authenticate each other directly โ there is no federation "hub" or registry.
E2EE-Preserving
The federation layer transmits opaque encrypted payloads. MLS group secrets and Double Ratchet sessions stay on client devices. The receiving server never sees plaintext.
Cryptographically Authenticated
Every request is signed with Ed25519. Receiving servers verify the signature against the sender's public key before processing. Replay attacks are blocked by a ยฑ5 minute timestamp window.
Reliable Delivery
Events are persisted in a transactional outbox and delivered with exponential backoff retry (up to 10 attempts). The inbox deduplicates events so processing is idempotent.
loqa.chat
community.org
02Server Identity & Discovery
Every federation-enabled Loqa instance publishes a discovery document at a well-known URL. This document is the root of trust โ it tells other servers how to reach us, which public key to verify our signatures against, and what capabilities we support.
Discovery Document
{
"server_name": "loqa.chat",
"federation_url": "https://loqa.chat/_federation",
"public_key": "ed25519:aB3dEf7G:dGhlIHB1YmxpYyBrZXkgYmFzZTY0...",
"version": "1.0",
"capabilities": [
"messages",
"members",
"reactions",
"typing",
"mls_e2ee",
"guild_transfer"
]
}
Ed25519 Keypair
Each server generates an Ed25519 signing keypair on first boot. The private key is encrypted with AES-256-GCM using the server's master encryption key before storage. The public key and a key_id fingerprint (first 6 bytes, base64) are published in the discovery document.
Key Storage
Private keys are stored in the federation_keys table as hex(nonce):base64(ciphertext). The 12-byte AES-GCM nonce is unique per key. Keys can be rotated โ old keys are marked expired but retained for signature verification during the transition window.
Capability Negotiation
The capabilities array declares which federation features this instance supports. Peers check capabilities before sending events โ e.g., a server without guild_transfer will never receive transfer offers.
Server Name Validation
When fetching a remote discovery document, the server_name field is validated against the requested domain. A mismatch generates a warning โ preventing DNS hijacking from silently redirecting federation traffic.
03HTTP Signature Authentication
Every federation request carries three custom HTTP headers that cryptographically bind the request to the sending server. The receiving server verifies the signature before processing any event.
METHOD\nPATH\nTIMESTAMP\nSHA256(body)Server's private signing key
Origin + Timestamp + Signature
Request Headers
| Header | Format | Purpose |
|---|---|---|
X-Loqa-Origin | loqa.chat | Identifies the sending server (used to look up its public key) |
X-Loqa-Timestamp | Unix epoch seconds | Binds signature to a point in time โ prevents replay outside ยฑ5 min window |
X-Loqa-Signature | ed25519:<fingerprint>:<base64> | Ed25519 signature over the constructed payload |
Verification Flow
|now โ timestamp| โค 300 seconds
FROM federation_peers WHERE status = 'active'
METHOD + PATH + TIMESTAMP + SHA256(body)
401 Unauthorized on any failure
Replay Protection
The ยฑ5 minute timestamp window ensures that intercepted requests cannot be replayed later. The body hash (SHA-256) is included in the signed payload, so modifying the request body invalidates the signature.
Key Rotation
The key_id in the signature header identifies which public key to verify against. When a server rotates its signing key, it publishes the new key in its discovery document. Old keys remain valid for signature verification until explicitly expired.
04Event System
The event system is the core of Loqa federation. Local actions (sending a message, adding a reaction, joining a guild) trigger hooks that fan out signed event envelopes to all peer servers via a persistent outbox.
Event Types
| Event Type | Trigger | Delivery | Data Fields |
|---|---|---|---|
MESSAGE_CREATE | User sends a message | Outbox | id, channel, author, content, encrypted_content, nonce, attachments, embeds |
MESSAGE_UPDATE | User edits a message | Outbox | id, channel, updates |
MESSAGE_DELETE | User deletes a message | Outbox | id, channel |
REACTION_ADD | User reacts to a message | Outbox | message_id, channel_id, emoji, user_id |
REACTION_REMOVE | User removes a reaction | Outbox | message_id, channel_id, emoji, user_id |
MEMBER_JOIN | User joins a guild | Outbox | guild_id, user_id |
MEMBER_LEAVE | User leaves a guild | Outbox | guild_id, user_id |
MEMBER_UPDATE | Profile or role change | Outbox | guild_id, user_id, updates |
TYPING_START | User starts typing | Direct (ephemeral) | channel_id, user_id |
GUILD_TRANSFER | Guild migrated to new server | Outbox | guild_id, new_authority, origin |
CHANNEL_CREATE | Channel added to guild | Outbox | guild_id, channel (id, name, type, position, topic) |
CHANNEL_UPDATE | Channel metadata changed | Outbox | guild_id, channel_id, updates |
CHANNEL_DELETE | Channel removed | Outbox | guild_id, channel_id |
ROLE_CREATE | Role added to guild | Outbox | guild_id, role (id, name, color, permissions) |
ROLE_UPDATE | Role properties changed | Outbox | guild_id, role_id, updates |
ROLE_DELETE | Role removed | Outbox | guild_id, role_id |
GUILD_UPDATE | Guild metadata changed | Outbox | guild_id, updates (name, description, icon) |
Event Envelope
{
"event_id": "msg_create_abc123",
"origin": "loqa.chat",
"timestamp": 1739836800,
"guild_id": "guild_456",
"event_type": "MESSAGE_CREATE",
"data": { ... },
"signature": "ed25519:aB3dEf7G:base64_signature..."
}
Outbox & Inbox Architecture
Local DB write + Centrifugo push
Fire-and-forget, never blocks API
Persistent queue in PostgreSQL
Every 5s, batch of 50
Verify signature + peer status
ON CONFLICT (event_id) DO NOTHING
Dispatch by event type
Federated message/member stored
Exponential Backoff
Failed deliveries are retried with exponential backoff: 2n seconds per attempt, capped at ~4 minutes (28 = 256s). After 10 failed attempts, the event is abandoned. Events for inactive peers are immediately dropped.
Idempotent Processing
Every event carries a unique event_id. The inbox table uses ON CONFLICT (event_id) DO NOTHING โ if the same event arrives twice (e.g., due to retry), the duplicate is silently discarded.
Fan-Out
Each guild can be federated with multiple peers. The hook layer queries federation_guild_peers and enqueues one outbox entry per peer. Direction control (inbound, outbound, both) limits which peers receive which events.
Ephemeral Events
TYPING_START is the only ephemeral event โ it bypasses the outbox entirely and is sent directly via HTTP. If delivery fails, it's silently dropped. Typing indicators are not persisted on either side.
05Peer Management
Federation peers are established through an invite โ accept handshake. Each peer relationship is scoped to specific guilds with configurable direction control.
POST /api/admin/federation/peers
GET https://target/.well-known/loqa/federation
POST /_federation/v1/peers/invite
{ origin_server, guild_id, public_key, direction }
Admin approval required on target
POST /_federation/v1/peers/accept
Returns server_name, public_key, federation_url
Per-Guild Scoping
Peer relationships are linked to specific guilds via federation_guild_peers. A single peer connection can federate multiple guilds, each with independent direction settings.
Direction Control
Each guild-peer link has a direction: inbound (receive only), outbound (send only), or both. This allows read-only mirrors, one-way broadcasting, or full bidirectional federation.
Health Checks
GET /_federation/v1/peers/status returns the server's health, protocol version, and supported capabilities โ enabling peers to verify connectivity and feature compatibility.
Public Key Exchange
On accept, both servers exchange their Ed25519 public keys. These keys are stored in federation_peers and used for all subsequent signature verification. Key rotation triggers a discovery document refresh.
06Guild Transfer Protocol
Loqa supports seamless guild migration between instances โ moving an entire community (channels, roles, members, emoji, bans) from one server to another without data loss.
POST /api/servers/:id/federation/transferPOST /_federation/v1/guilds/:id/transfer/offerIncludes guild preview (name, description, member count)
POST /_federation/v1/guilds/:id/transfer/snapshotContains guild, channels, roles, members, emoji, bans
Guild is created with
federation_enabled = trueGUILD_TRANSFER event to all peersClients are redirected to the new authority
Snapshot Contents
| Object | Fields | Conflict Strategy |
|---|---|---|
| Guild | name, owner_id, description, icon | ON CONFLICT (id) DO UPDATE |
| Channels | id, name, type, position, topic | ON CONFLICT (id) DO NOTHING |
| Roles | id, name, rank, color, permissions | ON CONFLICT (id) DO NOTHING |
| Members | user_id, username, display_name, avatar, nickname, roles | Via federated_users table |
| Emoji | guild_id, name, animated, url | ON CONFLICT DO NOTHING |
| Bans | server_id, user_id, reason | ON CONFLICT DO NOTHING |
Tombstone Redirect
After a successful transfer, the origin server stores a federation_transfers record with a redirect_until timestamp. Any API requests for the transferred guild return a redirect to the new authority, giving clients time to update.
Peer Notification
A GUILD_TRANSFER event is broadcast to all federation peers via the outbox. Peers update their federation_guild_peers records to point to the new authority โ future events for this guild route to the new server.
07E2EE Compatibility
The federation protocol is designed from the ground up to preserve end-to-end encryption. Neither the sending nor receiving server can read encrypted message content โ they simply relay opaque ciphertext.
Instance A
Double Ratchet encrypt
encrypted_contentnoncesender_key_idcontent: nullInstance B
Double Ratchet decrypt
MLS Groups Span Instances
MLS group state lives entirely on client devices via OpenMLS WASM. When a message is sent to a federated guild, the encrypted_content and nonce fields are carried verbatim through the federation layer โ the receiving instance stores them as-is for its local clients to decrypt.
Federated User IDs
Users from remote instances are identified as user_id@origin_server (e.g., [email protected]). This namespacing prevents ID collisions and makes it clear which instance is authoritative for each user's identity.
Media Proxy
When media_proxy is enabled (default: true), remote media URLs are proxied through the local server. This prevents IP leakage โ clients never make direct requests to remote instances, preserving user privacy.
Origin Tracking
Every federated message is tagged with federation_origin in the database. Updates and deletes are scoped to matching origin servers โ a remote server can only modify or delete its own messages, never local ones.
08Connecting Your Instance
This section walks through the practical steps for self-hosting operators who want to federate their Loqa instance with the main network or another operator.
Prerequisites
1. Running Loqa Instance
You need a running Loqa backend (Rust) with PostgreSQL. The instance must be reachable via HTTPS on a domain you control โ federation endpoints are served under /_federation/v1/*.
2. TLS Certificate
A valid TLS certificate for your domain. Self-signed certs are rejected โ use Let's Encrypt or similar. Federation traffic requires TLS 1.2+ (1.3 recommended).
Step-by-Step Setup
Set
federation.enabled = true and federation.server_name to your domainEd25519 keypair created on first boot, encrypted and stored in DB
GET https://yourdomain/.well-known/loqa/federation should return JSON with your public key
POST /api/admin/federation/peers with the target domain โ your server will fetch their discovery doc and send a signed invite
Peer status changes from 'pending' โ 'active' and public keys are exchanged
POST /api/admin/federation/guilds to associate specific guilds with the peer, setting direction (inbound/outbound/both)
Configuration Reference
| Setting | Default | Description |
|---|---|---|
federation.enabled | false | Master switch โ enables the /_federation router and discovery endpoint |
federation.server_name | โ | Your domain (e.g., community.org). Must match your TLS certificate. |
federation.media_proxy | true | Proxy remote media through your server (prevents IP leakage) |
federation.outbox_drain_interval_secs | 5 | How often the outbox drain task runs (seconds) |
federation.max_retry_attempts | 10 | Max delivery retries before an event is abandoned |
Reverse Proxy
If you run behind Caddy, Nginx, or similar, ensure /_federation/* and /.well-known/loqa/* are forwarded to the Loqa backend. The discovery document must be publicly accessible without authentication.
Firewall Rules
Open port 443 for inbound HTTPS. Federation uses standard HTTPS โ no custom ports or protocols. Outbound requests go to port 443 of peer domains.
Health Verification
After peering, hit GET /_federation/v1/peers/status on both sides to confirm connectivity. You should see the peer listed as active with matching protocol versions.
Troubleshooting
If events aren't flowing, check: (1) peer status is active, (2) guild is linked with correct direction, (3) federation_enabled = true on the guild's server, (4) outbox drain task is running (check logs for Federation event queued).
09Reproducible Audit Scope
Every protocol claim in this whitepaper can be independently verified by reading 14 self-contained Rust source files. The federation crate has zero business logic dependencies โ it relies only on standard cryptographic libraries (ed25519-dalek, aes-gcm, sha2) and PostgreSQL.
| File | Purpose | Lines |
|---|---|---|
lib.rs | Crate root โ FederationState initialization, module exports | 62 |
auth.rs | HTTP signature construction & verification (Ed25519 + SHA-256) | 84 |
signing.rs | Ed25519 key generation, AES-256-GCM encrypted storage, signing & verifying | 127 |
config.rs | Federation configuration (server_name, media_proxy, retry limits) | 44 |
discovery.rs | Remote .well-known document fetching & validation | 56 |
transport.rs | Outbound signed HTTP POST transport | 41 |
outbox.rs | Persistent event queue with exponential backoff retry | 133 |
hooks.rs | Fire-and-forget hooks โ fan-out events to peers after local writes | 313 |
processor.rs | Inbound event processor โ dedup, dispatch, and per-type handlers | 488 |
routes.rs | Axum router โ /_federation/v1/* endpoints | 157 |
handlers/peers.rs | Peer invite, accept, and health check handlers | 132 |
handlers/guild.rs | Guild state snapshot export with peer authentication | 240 |
handlers/transfer.rs | Guild transfer โ offer, snapshot import, tombstone, peer notification | 280 |
handlers/mod.rs | Handler module re-exports | 4 |
| Total auditable surface | 2,161 | |
For security researchers: These 14 files contain 100% of the federation protocol logic. No federation decisions are made outside this boundary. Every signature, every event dispatch, every peer authentication check is in these files.
Questions About Federation?
We welcome review from security researchers, self-hosting operators, and anyone interested in decentralized communication. We're happy to provide additional technical detail or discuss deployment.