Skip to content

Server Discovery API

Server discovery enables clients and servers to automatically discover capabilities, configuration, and operational status of Cryptid servers. This follows the .well-known standard (RFC 5785) used by many federated protocols.

All Cryptid API endpoints are relative to the server’s base URL:

https://chat.example.com

URL Structure:

  • Protocol: https:// (TLS required, no HTTP)

  • Domain: Server’s fully qualified domain name

  • No path prefix (API endpoints start at root)

Example URLs:

  • Device announcement: https://chat.example.com/v1/announce

  • WebSocket stream: wss://chat.example.com/v1/stream

  • Server discovery: https://chat.example.com/.well-known/cryptid

Discover server capabilities, configuration, and federation parameters. This endpoint is publicly accessible (no authentication required) and is used by:

  • Clients choosing which server to connect to

  • Servers discovering federation capabilities of remote servers

  • Monitoring tools checking server availability

Standard: Follows RFC 5785 (.well-known URI) convention used by protocols like Matrix, Mastodon, and WebFinger.

GET /.well-known/cryptid
GET /.well-known/cryptid HTTP/1.1
Host: server.example.com
{
"protocol_version": "1.0",
"server_name": "server.example.com",
"federation_enabled": true,
"supported_features": ["mls_messaging"],
"rate_limits": {
"federated_messages_per_minute": 1000,
"announcements_per_hour": 24
},
"message_limits": {
"max_message_size": 10485760,
"max_retention_days": 30
},
"certificate_fingerprint": "sha256:a1b2c3d4e5f6172839..."
}

Response Fields:

  • protocol_version: Cryptid protocol version this server implements (semantic versioning)

    • Clients should check compatibility (e.g., reject servers with incompatible major versions)
  • server_name: Server’s canonical domain name (must match DNS)

    • Used for device address validation (e.g., device_id@server_name)
  • federation_enabled: Whether this server accepts federated messages from other servers

    • true: Server accepts federation requests at /federation/v1/*
    • false: Server is isolated (no cross-server messaging)
  • supported_features: Array of optional features this server implements

    • mls_messaging: MLS-encrypted messaging (required)
    • websocket_streaming: Real-time WebSocket delivery
    • offline_queuing: Stores messages for offline devices
    • Future: relay_network, multi_device_sync, etc.
  • rate_limits: Server’s rate limiting policies

    • federated_messages_per_minute: Max messages/minute in federation requests
    • announcements_per_hour: Max device announcements per hour per address
  • message_limits: Message storage and size constraints

    • max_message_size: Maximum message size in bytes (default: 10MB)
    • max_retention_days: How long unacknowledged messages are kept (default: 30 days)
  • certificate_fingerprint: SHA-256 fingerprint of server’s TLS certificate

    • Used for certificate pinning and trust-on-first-use (TOFU)
    • Format: sha256: prefix + lowercase hex (64 characters)
async fn choose_server(candidates: Vec<String>) -> Result<String> {
for domain in candidates {
let caps = discover_server(&domain).await?;
// Check requirements
if caps.protocol_version.starts_with("1.") && caps.supported_features.contains(&"websocket_streaming".to_string()) {
return Ok(domain);
}
}
Err(anyhow!("No compatible server found"))
}
async fn can_federate_with(domain: &str) -> Result<bool> {
let caps = discover_server(domain).await?;
if !caps.federation_enabled {
return Ok(false);
}
// Check protocol version compatibility
if caps.protocol_version.starts_with("1.") {
return Ok(true);
}
Ok(false)
}
async fn verify_server_certificate(domain: &str, observed_fingerprint: &str) -> Result<()> {
let caps = discover_server(domain).await?;
if caps.certificate_fingerprint != observed_fingerprint {
return Err(anyhow!("Certificate fingerprint mismatch! Possible MITM attack!"));
}
Ok(())
}

Server does not implement Cryptid or has disabled discovery.

{
"error": "ENDPOINT_NOT_FOUND",
"message": "Server does not support Cryptid protocol"
}

Server is operational but is temporarily not accepting new connections.

{
"error": "SERVER_UNAVAILABLE",
"message": "Server temporarily unavailable"
}

Check server operational status and uptime. Used by monitoring tools, load balancers, and administrators.

Use Cases:

  • Load balancer health checks

  • Monitoring/alerting systems

  • Admin dashboards

  • Client fallback logic (try different server if unhealthy)

GET /v1/health
GET /v1/health HTTP/1.1
{
"status": "healthy",
"timestamp": 1759000000,
"version": "1.0.0",
"uptime": 2592000
}

Response Fields:

  • status: Server health status

    • healthy: Server fully operational
    • degraded: Server operational but experiencing issues (e.g., high load, partial service)
    • unhealthy: Server experiencing critical issues (still responding but unreliable)
  • timestamp: Current server Unix timestamp

    • Used for client clock synchronization validation
  • version: Server software version (semantic versioning)

    • Format: major.minor.patch (e.g., 1.2.3)
    • Useful for debugging and compatibility tracking
  • uptime: Server uptime in seconds since last restart

    • Example: 2592000 = 30 days

200 OK: Server is healthy and operational

503 Service Unavailable: Server is unhealthy or under maintenance

{
"status": "unhealthy",
"timestamp": 1759000000,
"version": "1.0.0",
"uptime": 3600,
"details": "Database connection failed"
}

Load balancers should remove servers from rotation upon a 503.

Server capabilities change infrequently and should be cached.

struct ServerCapabilitiesCache {
cache: HashMap<String, (ServerCapabilities, Instant)>,
ttl: Duration,
}
impl ServerCapabilitiesCache {
async fn get(&mut self, domain: &str) -> Result<ServerCapabilities> {
if let Some((caps, cached_at)) = self.cache.get(domain) {
if cached_at.elapsed() < self.ttl {
return Ok(caps.clone());
}
}
// Cache miss or expired, fetch new
let caps = discover_server(domain).await?;
self.cache.insert(domain.to_string(), (caps.clone(), Instant::now()));
Ok(caps)
}
}

Recommended TTL: 1h for server capabilities, 30s for health checks.

Cryptid servers MUST:

  1. Have valid DNS records pointing to their domain

  2. Use TLS 1.3+ with valid certificates (no self-signed in production)

  3. Serve .well-known/cryptid over HTTPS (no HTTP fallback)

This prevents:

  • DNS spoofing attacks

  • Man-in-the-middle attacks

  • Impersonation of legitimate servers

The certificate_fingerprint field enables Trust on First Use (TOFU):

  1. Client connects to server for first time

  2. Client records certificate fingerprint from .well-known/cryptid

  3. On subsequent connections, client verifies fingerprint hasn’t changed

  4. If changed, client alerts user (possible MITM or legitimate rotation)

Calculating fingerprint:

use sha2::{Sha256, Digest};
fn calculate_fingerprint(cert_der: &[u8]) -> String {
let hash = Sha256::digest(cert_der);
format!("sha256:{}", hex::encode(hash))
}

Cryptid uses semantic versioning for protocol_version:

  • Major version changes break compatibility (e.g., 1.x -> 2.x)

  • Minor version adds features in a backwards compatible way (e.g., 1.0 -> 1.1)

  • Patch version fixes bugs (e.g., 1.0.0 -> 1.0.1)

Compatibility rules:

  • Clients MUST check major version matches

  • Clients MAY check minor version for required features

  • Clients SHOULD ignore patch version for compatibility decisions

Clients SHOULD respect advertised rate limits from server capabilities.