Overview
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.
Scope
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
remoteServerfield on each federatedUserInfoidentifies 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.
Security
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.
Setup
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.
Protocol
Event flow.
Join / Leave
Snapshot on wake
When a Durable Object wakes from hibernation and the first local user joins presence, it doesn't know the remote state:
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.
API
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" }
Wire format
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.
Peering
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.
Reliability
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.
Caveats
Known limitations (Phase 1).
-
Static peers only. No dynamic peer discovery, no gossip protocol. In
openmode, adding a peer is fully automatic. Inallowlistmode, the self-hoster submits a request vialoci federate addand 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/statsreflects 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.
What's next