Chapter 16. Federated Protocols

Two federated protocols are in wide use for social networking: ActivityPub, behind Mastodon, and the AT Protocol, behind Bluesky. Both set out to do what R2 asks, which is to let independent parties share state with no central owner. Local-first software pursues the same goal on the user’s own device. This chapter scores the two protocols and examines local-first software in prose.

ActivityPub

ActivityPub became a W3C Recommendation in January 2018. Every object and actor has an HTTPS URI under its originating server, and the specification requires those URIs to be publicly dereferenceable. An actor posts to an outbox and receives in an inbox, and servers deliver activities to each other’s inboxes. The payload format is ActivityStreams 2.0 (2017), which is JSON-LD, so the data has an RDF reading.

That reading is optional. ActivityStreams lets implementations process documents as plain JSON, and most do. Mastodon’s own documentation notes that it maps one vocabulary prefix to the wrong namespace, so a processor that uses the correct context misreads its profile fields. A plain-JSON reader sees nested trees with string keys, which Chapter 10 has already scored.

The column scores the protocol as specified and notes where deployments fall short. R1 holds. R2 is partial: read as RDF, two servers’ data merges by union, but the protocol defines no merge and deployments do not perform one. R3 is partial: object identifiers are URIs, but property names are global only through a context that implementations often ignore. S4 is partial: objects, actors, and collections dereference by GET, but a timeline or a search result is not a standard resource. No query or transformation language is specified, so S2 fails. S1 and S3 are partial for the same reasons as JSON/REST’s: endpoints separate selection, and servers substitute only behind a shared contract.

The AT Protocol

The AT Protocol underlies Bluesky, and an IETF working group chartered in March 2026 is standardizing its core. Each account has one repository: a signed tree of records, stored on the account’s server. A record is named by an at:// URI built from the account’s DID, a collection, and a record key. Records follow Lexicon schemas, each named by a reverse-DNS identifier such as app.bsky.feed.post.

These names reach further than JSON’s. DIDs and at:// URIs are global, and Lexicon types them as references, with the string formats did and at-uri. But property names inside a record are plain keys scoped to the record’s schema, and the charter puts application schemas out of scope. Minting is also less free than a URI’s. The default DID method, did:plc, registers every identity with a central directory server. And an at:// authority “does not indicate a network location,” so a record does not dereference by GET. R3 is partial.

There is no merge. Repositories stay separate signed trees. Relays combine many repositories into one event stream, and AppViews index that stream into the views an application shows. The combining is application code, one AppView per application, so R2 fails. Queries run as HTTP GET under /xrpc/, so a query result has a URL, but the URL names an endpoint on one host rather than the data (S4 ~). Lexicon specifies schemas and endpoints but no query or transformation language (S2 ✗). S1 and S3 are partial, as for JSON/REST.

Local-first software

Local-first software keeps the primary copy of the data on the user’s device and syncs replicas with no server in charge (Kleppmann et al., 2019). Its tool is the CRDT, the structure Chapter 5 cited as independent support for the merge laws. Automerge, a CRDT library, shows how far the laws reach. Its merge is total, order-free, and idempotent, but only among replicas of one document. Two documents that began separately do not merge, because a document’s meaning lives in its tree: a root map of string keys over nested maps and lists. That is B-2d failing, as it failed for JSON in Chapter 10. Keys are plain strings and object identifiers are local to the document, so R3 fails too.

Deletes raise the problem Prop. 7.5 addresses. When one replica deletes a key and another sets it, Automerge keeps the new value. That is a policy chosen so that concurrent writes converge. The derived model needs no such policy between parties, because each party removes only its own facts. Local-first software gets no column below, because it defines no stages to score on S1–S4. On the R-rows it matches the JSON trees it is built from, with a merge added inside one document.

The two columns

ActivityPub AT Protocol
R1 ✓ — an extensible vocabulary encodes any domain ✓ — Lexicon schemas encode any domain
R2 ~ — union available through the RDF reading; no merge defined or performed ✗ — no merge; repositories combine only in application indexes
R3 ~ — identifiers are URIs; property names global only through an often-ignored context ~ — DIDs and at:// URIs are global and typed; property names are schema-local; did:plc minting goes through a central directory
S1 ~ — endpoints separate; representations fuse arrangement and data ~ — query endpoints separate; AppViews fuse arrangement
S2 ✗ — no query or transformation language ✗ — schemas and endpoints, no query or transformation language
S3 ~ — substitution behind the shared protocol contract ~ — substitution behind a Lexicon’s contract
S4 ~ — objects and collections dereference; selections do not ~ — query results have GET URLs on one host; records do not dereference

Both protocols score above JSON/REST on R3, because both name things globally. ActivityPub comes closest overall, because its payload already has the derived model’s reading. Neither defines a merge or a query language. In both, federation is delivery between servers, and each server or AppView rebuilds the combined view in its own code. That rebuilding is the compensating industry again, this time built into the protocol.