Skip to content

Federation API

Federation enables Cryptid servers to route messages across different domains, creating a truly decentralized messaging network. Servers authenticate with each other using server certificates, then relay encrypted messages without ever understanding their content or relationships.

Key Principle: Federated servers act as encrypted message relays. They forward opaque MLS ciphertext between domains without knowing senders, content, or social graphs.

Delivers messages from another Cryptid server to local recipients. The receiving server validates the federation token, checks that recipients are announced locally, and queues messages for delivery.

Device A Server A Server B Device B
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ 1. Announce (Register) │ │ │
├───────────────────────────►│ │ │
│ │ │ │
│ │ │ │
│ JWT Token │ │ │
│◄───────────────────────────┤ │ │
│ │ │ │
│ │ │ │
│ 2. Send encrypted msg │ │ │
├───────────────────────────►│ │ │
│ │ │ │
│ │ 3. Federation │ │
│ ├─────────────────────────────────►│ │
│ │ (relay) │ │
│ │ │ 4. Queue msg │
│ │ ├─────────────────────────────►│
│ │ │ │
│ │ │ │
│ │ │ 5. Retrieve (WebSocket) │
│ │ │◄─────────────────────────────┤
│ │ │ │

Neither Server A nor Server B learns message content or sender identity.

PUT /federation/v1/deliver/{transaction_id}
PUT /federation/v1/deliver/unique_transaction_id HTTP/1.1
Content-Type: application/json
Authorization: Bearer federation_token
X-Origin-Server: alice.server1.com
{
"origin_server": "alice.server1.com",
"transaction_id": "unique_transaction_id",
"timestamp": 1759000000,
"messages": [
{
"message_id": "32_byte_message_id_hex",
"recipient_address": "bob@server2.com",
"group_id": "32_byte_group_id_hex",
"mls_ciphertext": "base64_encrypted_content",
"sender_signature": "ed25519_signature",
"timestamp": 1758999498,
"message_type": "Text"
}
]
}

Parameters:

  • origin_server: Domain of the originating server (must match X-Origin-Server header)

  • transaction_id: Unique transaction ID (typically UUIDv4)

  • timestamp: Unix timestamp of the federation request (not message timestamp)

  • messages[]: Array of encrypted messages to deliver

Message Fields:

  • recipient_address: Must be a device announced on the receiving server

  • mls_ciphertext: Encrypted payload (opaque to both servers)

  • sender_signature: Ed25519 signature (server does not verify, recipient does)

The sender’s address is intentionally NOT included in the federated messages to protect sender privacy. Recipients can derive sender identity from the MLS group context or signature validation.

{
"transaction_id": "unique_transaction_id",
"status": "accepted",
"accepted_messages": 1,
"rejected_messages": 0,
"timestamp": 1759000000
}

Response Fields:

  • status:

    • accepted: All messages queued successfully
    • partial: Some messages rejected (see rejected_messages)
    • rejected: All messages rejected
  • accepted_messages: Count of messages successfully queued

  • rejected_messages: Count of messages rejected (recipient not announced, invalid format, etc)

  • timestamp: Server’s current timestamp

Partial Success: If some recipients are not announced locally, those messages are rejected while others are accepted.

Server error notifications:

{
"type": "error",
"error": "INVALID_TOKEN",
"message": "Federation token invalid or expired",
"code": 4008
}

Common Errors:

  • 4001: Invalid signature/certificate
  • 4004: Rate limit exceeded
  • 4006: Federation failed
  • 4008: Token expired

See Error Handling for complete error code reference.

Authenticates a server for cross-server message delivery. Servers must prove ownership of their domain before they can relay messages, preventing impersonation attacks.

Certificate Validation: The receiving server validates that the origin server controls the claimed domain using the provided server certificate.

POST /federation/v1/auth
POST /federation/v1/auth HTTP/1.1
Content-Type: application/json
{
"origin_server": "alice.server1.com",
"server_certificate": "base64_server_certificate",
"challenge_response": "proof_of_server_ownership"
}

Parameters:

  • origin_server: Domain of the server requesting federation access

  • server_certificate: Base64 encoded server certificate (TLS certificate or equivalent)

  • challenge_response: Cryptographic proof of domain ownership

Challenge-Response Flow:

  1. Origin server requests authentication with certificate

  2. Receiving server validates certificate matches origin_server domain

  3. Receiving server generates random challenge: nonce = random_bytes(32)

  4. Origin server signs challenge with server private key: signature = sign(server_key, nonce)

  5. Receiving server verifies signature with certificate public key

  6. Receiving server issues time-limited federation token

Implementation Note: The challenge_response field contains the signed challenge. The exact challenge mechanism is implementation-specific, but must cryptographically prove server ownership.

{
"federation_token": "jwt_federation_token",
"expires_at": 1759003600,
"rate_limits": {
"messages_per_minute": 1000,
"transactions_per_minute": 100
}
}

Response Fields:

  • federation_token: JWT token for authenticated federation requests

  • expires_at: Unix timestamp when token expires (typically 1 hour)

  • rate_limits: Server’s federation rate limits

    • messages_per_minute: Total individual messages allowed per minute

    • transactions_per_minute: Total federation requests allowed per minute

Token Usage: Include this token in the Authorization: Bearer {federation_token} header for all /federation/v1/deliver requests.

Rate Limits Explained:

  • If you send 10 messages in 1 transaction, you consume: 1 transaction, 10 messages

  • If you send 1 message in 10 transactions, you consume: 10 transactions, 10 messages

  • Best Practice: Batch messages efficiently (e.g., 10 messages/transaction)

{
"error": "INVALID_CERTIFICATE",
"message": "Server certificate invalid or domain mismatch",
"code": 4001
}
{
"error": "FEDERATION_DISABLED",
"message": "This server does not support federation",
"code": 5003
}

Efficient:

{
"messages": [
// 10 messages for different recipients
]
}
// Uses: 1 transaction, 10 messages

Inefficient:

{
// 10 separate API calls, each with 1 message
// Uses: 10 transactions, 10 messages
// Hits transaction limit 10x faster
}

Recommendation: Batch up to 50-100 messages per transaction for optimal efficiency.

Federation tokens typically expire after 1 hour. Cache them and reuse.

struct FederationClient {
tokens: HashMap<String, CachedToken>,
}
struct CachedToken {
token: String,
expires_at: u64,
}
impl FederationClient {
async fn get_token(&mut self, target_domain: &str) -> Result<String> {
if let Some(cached) = self.tokens.get(target_domain) {
let now = current_timestamp();
// Refresh if expiring in < 5 minutes
if cached.expires_at > now && (cached.expires_at - now) > 300 {
return Ok(cached.token.clone())
}
}
// Authenticate and cache new token
let response = self.authenticate(target_domain).await?;
self.tokens.insert(target_domain.to_string(), CachedToken {
token: response.federation_token.clone(),
expires_at: response.expires_at,
});
Ok(response.federation_token)
}
}

Before federating with a server, discover its capabilities:

async fn discover_server(domain: &str) -> Result<ServerCapabilities> {
let url = format!("https://{domain}/.well-known/cryptid");
let response = reqwest::get(&url).await?;
let caps: ServerCapabilities = response.json().await?;
if !caps.federation_enabled {
return Err(anyhow!("Federation not supported"));
}
Ok(caps)
}

See Server Discovery for details on the .well-known/cryptid endpoint.

Federation requests can fail due to network issues or temporary server unavailability:

async fn deliver_with_retry(
target_domain: &str,
messages: Vec<Message>,
max_retries: u32,
) -> Result<FederationResponse> {
let mut retry_count = 0;
loop {
match deliver_messages(target_domain, &messages).await {
Ok(response) => return Ok(response),
Err(e) if retry_count < max_retries => {
retry_count += 1;
// Exponential backoff: 1s, 2s, 4s, 8s, ...
let delay = 2u64.pow(retry_count - 1);
tokio::time::sleep(Duration::from_secs(delay)).await;
}
Err(e) => return Err(e),
}
}
}

Transaction IDs prevent duplicate message delivery if federation requests are retried:

  • Must be unique per federation request

  • Recommended format: UUIDv4

  • Receiving server may cache transaction IDs for ~1 hour to detect duplicates

  • Retrying the same transaction ID returns the original response (idempotent)

Federation maintains Cryptid’s privacy principles:

Servers relay but never learn:

  • Message content (MLS encrypted end-to-end)

  • Sender identity (not included in federation request)

  • Social relationships (no group membership lists)

  • User metadata (no profiles, no presence information)

What servers DO know:

  • Recipient device address (required for delivery routing)

  • Origin server domain (e.g., alice.example.com, but not the specific device)

  • Which domains exchange messages (network metadata)

  • Message delivery timestamps (for routing only)

  • Approximate message volume between servers

This metadata is minimal and inherent to any federated system.

Production implementations must:

  1. Verify server certificate is signed by trusted CA

  2. Check certificate matches claimed domain (DNS validation)

  3. Validate certificate has not expired or been revoked

  4. Use TLS 1.3+ for all federation connections

  • Rate limiting prevents spam from malicious servers

  • Transaction ID deduplication prevents replay attacks

  • Token expiration limits impact of stolen credentials

Servers trust each other to:

  • Not inject fake messages (prevented by sender signatures)

  • Not drop messages maliciously (no guaranteed delivery in Cryptid)

  • Follow protocol honestly (Byzantine fault tolerance not guaranteed)