Federation · Phase 1 · Presence Only

Decentralised presence
across every Loci network.

One open protocol. Many independently-operated servers. When Loci networks federate, a single presence count reflects everyone on a URL — regardless of which server they connected through. Chat, voice, and moderation stay local. The awareness doesn't.


🔗
Phase 1 — Presence federation is live on loci.frnd.tech

Chat, voice, and RPG remain per-deployment. Cross-server presence is the only thing that crosses server boundaries in this phase.

Global awareness. Local conversation.

Loci's core promise is knowing who else is on the same URL right now. Without federation, that promise only works within a single deployment. With federation, it works across every trusted Loci server — you see everyone on a URL regardless of which Loci network they connected through.

Chat, voice, and games stay local. They belong to the community you joined, not to a merged global room nobody moderated.

Think of it like email: you can send a message to any address regardless of which provider hosts it — but your inbox is still yours. Loci federation does the same thing for presence.


What is (and isn't) federated.

Phase 1 federates presence only:

  • Participant count (count, localCount, federatedCount)
  • Participant cards: display name, connected layers, connected-at timestamp
  • The remoteServer field on each federated UserInfo identifies which peer network a user connected through

That's it. Nothing else crosses server boundaries.

Feature Federated? Reason
Presence counts & cards Yes Core protocol promise
Chat messages No Belongs to the community you joined
Audio / WebRTC signaling No Mesh topology is per-server; cross-server signaling adds latency and complexity
RPG state & battles No Anti-cheat depends on a single authoritative server
Claimed handles / OAuth No Identity trust is per-deployment
Moderation rules & reports No Each community moderates its own space
Global stats aggregation Future Planned for Phase 5

Federating chat would mean merging communities that opted into separate spaces. That's not the design. Federation adds awareness; it doesn't dissolve community boundaries.


Trust model.

Phase 1 uses static trusted peers with shared secrets. There is no PKI, no certificate pinning, and no dynamic peer discovery.

Each pair of servers shares a secret out-of-band. The secret is used to derive a bearer token:

token = hex(SHA-256("loci-fed:" + secret))

All federation requests are sent over HTTPS. The token goes in the Authorization: Bearer <token> header. Both sides derive the same token from the same secret — if they match, the request is authenticated.

What this doesn't protect against: if the shared secret is compromised, an attacker can inject fake presence events. Rotate secrets if you suspect compromise. HMAC-SHA256 with timestamps (replay protection) is planned for Phase 4.

Federation modes

Mode Behaviour Recommended for
off Federation disabled (default) Private deployments
allowlist Peering requests queue for admin approval Official network (loci.frnd.tech)
open Every inbound request is auto-approved Trusted closed networks

Open mode risks: Any server that discovers your URL can federate with you automatically. This can lead to presence inflation from unknown servers, fake display names from malicious peers, and spam presence events. Your only runtime defence is the blocklist (/admin/federation/block). Only use open mode in controlled environments where all potential peers are already trusted. allowlist mode is strongly recommended for public-facing servers.


Configuration.

Federation is disabled by default. No env var = no federation.

To enable, set LOCI_FEDERATION_PEERS in your Worker environment (wrangler secret, .dev.vars, or CI env):

// LOCI_FEDERATION_PEERS
[
  {
    "id":     "frnd-tech",
    "url":    "https://loci.frnd.tech",
    "secret": "a-long-random-shared-secret"
  },
  {
    "id":     "partner-deploy",
    "url":    "https://loci.partner.example.com",
    "secret": "another-different-secret"
  }
]

Also set LOCI_SERVER_ID to a stable, unique identifier for this server (used as originServer in emitted events):

# wrangler.toml (or set as a secret)
[vars]
LOCI_SERVER_ID = "frnd-tech"

Mutual configuration

Federation is bidirectional — both servers must configure each other as peers.

# Server A — loci.frnd.tech
[{ "id": "partner", "url": "https://loci.partner.example.com", "secret": "shared-xyz" }]

# Server B — loci.partner.example.com
[{ "id": "frnd-tech", "url": "https://loci.frnd.tech", "secret": "shared-xyz" }]

The shared secret must be the same on both sides. The id values are local labels — it helps to use recognisable names.


Event flow.

Join / Leave

Local user subscribes to presence on Server A ├─→ Server A emits FederatedPresenceJoin to all peers (fire-and-forget) └─→ Server B receives POST /federation/presence ├─ Authenticates bearer token ├─ Routes to LocusRoom DO for that lociId ├─ DO updates remotePresence map └─ DO broadcasts updated combined snapshot to local subscribers Local user disconnects from Server A ├─→ Server A emits FederatedPresenceLeave to all peers └─→ Server B removes user from remotePresence, re-broadcasts snapshot

Snapshot on wake

When a Durable Object wakes from hibernation and the first local user joins presence, it doesn't know the remote state:

First local user joins presence on Server A (room was hibernating) └─→ Server A GETs /federation/presence?lociId=... from each peer └─→ Peer responds with FederatedPresenceSnapshot └─→ Server A seeds remotePresence from snapshot └─→ Combined snapshot sent to local client

Event deduplication

Each event carries a unique eventId (UUID). Each room keeps a bounded FIFO set of the last 200 seen event IDs. Duplicate delivery — from a retry, for example — is dropped without processing.


Federation HTTP endpoints.

These endpoints are server-to-server only. All authenticated routes require a valid Authorization: Bearer <token> header. Unauthenticated requests receive 401.

Method Path Auth Description
POST /federation/presence Required Receive a presence event (join, leave, or snapshot) from a peer
GET /federation/presence?lociId=<id> Required Return this server's current presence snapshot for a room
GET /federation/health Required Connectivity check — returns { ok: true, serverId, version }
POST /federation/request None Submit a peering request (open / allowlist modes)
GET /federation/request/:requestId None Check request status; returns secret on first poll after approval (one-time pickup, 24 h TTL)

Status endpoint response shapes

// Still awaiting admin approval
{ "status": "pending" }

// Approved — secret returned and immediately consumed (one-time)
{
  "status": "approved",
  "secret": "<64-char hex>",
  "peer": { "serverId": "frnd-tech", "url": "https://loci.frnd.tech" }
}

// Not found or already consumed
{ "status": "not_found", "error": "Request not found or already consumed" }

Presence API response breakdown.

GET /api/presence?url=https://example.com/page returns:

{
  "type": "presence",
  "count": 14,              // total (local + federated)
  "localCount": 3,          // present only when federation is enabled
  "federatedCount": 11,     // present only when federation is enabled
  "users": [
    {
      "sessionId": "abc",
      "displayName": "Silent Crane",
      "layers": ["presence", "chat"],
      "connectedAt": 1712345678000
    },
    {
      "sessionId": "xyz",
      "displayName": "Quiet Falcon",
      "layers": ["presence"],
      "connectedAt": 1712345600000,
      "remoteServer": "frnd-tech"  // present only on federated users
    }
  ]
}
  • count — total (local + federated). Existing consumers are unaffected.
  • localCount / federatedCount — present only when federation is enabled.
  • remoteServer — present only on federated users. Clients that ignore unknown fields continue to work unchanged.

Joining the official network.

The full self-hoster experience when joining loci.frnd.tech via the CLI:

# Step 1 — submit a peering request
loci federate add https://loci.frnd.tech \
  --api https://my-loci.example.com \
  --admin-secret $MY_ADMIN_SECRET

→ status: pending, requestId: abc-123
→ "Run: loci federate activate abc-123 ..."

# Step 2 — admin approves (on the target server)
loci federate approve abc-123 \
  --api https://loci.frnd.tech \
  --admin-secret $FRND_ADMIN_SECRET

→ secret generated, stored in KV for 24 hours

# Step 3 — activate on your server
loci federate activate abc-123 \
  --target https://loci.frnd.tech \
  --api https://my-loci.example.com \
  --admin-secret $MY_ADMIN_SECRET

→ fetches secret from loci.frnd.tech/federation/request/abc-123
→ stores peer in my-loci.example.com KV
→ federation established on both sides

No manual secret sharing. The requestId is the one-time pickup token — it expires after 24 hours or on first use. No out-of-band coordination required.


Remote user TTL.

Remote presence entries have a 90-second TTL. Each received event refreshes a user's lastSeenAt timestamp. The Durable Object alarm (runs every 30 s) evicts entries older than 90 s.

If a peer goes silent — crash, network partition — its users fade out within 90 s. No explicit error state. The entries simply stop being refreshed and eventually expire.


Known limitations (Phase 1).

  • Static peers only. No dynamic peer discovery, no gossip protocol. In open mode, adding a peer is fully automatic. In allowlist mode, the self-hoster submits a request via loci federate add and picks up the approved secret once an admin approves it — no out-of-band secret sharing required.
  • No relay. If A federates with B and B federates with C, A and C do not see each other's presence unless they're also directly peered. There is no event relay and no loop-amplification risk.
  • No token rotation. Shared secrets are static. Rotate manually if compromised.
  • No global stats aggregation. /api/stats reflects local stats only.
  • Presence only. Chat, audio, and RPG remain per-deployment forever in this phase.
  • Fire-and-forget delivery. Events are not queued or retried. A peer that is temporarily unreachable will miss join/leave events and rely on TTL expiry or the next snapshot request to resync.

Federation roadmap.

Phase 1
Presence federation — live Static peers, shared secrets, fire-and-forget delivery, 90 s TTL
Phase 2
Retry / at-least-once delivery Queued federation events with backoff and acknowledgement
Phase 3
Dynamic peer discovery A shared registry that lets Loci servers find each other without manual config
Phase 4
HMAC-SHA256 with nonce Replay-attack resistance — short-lived signatures on every federation request
Phase 5
Cross-server stats aggregation Global active-room and participant counts across the federated network