ATProto and the Serverless Social Web

Users are increasingly frustrated by losing their identity and data when social platforms fail or change direction. The Authenticated Transfer Protocol (ATProto) ecosystem addresses this by giving users ownership of their data and identities, with all published content forming part of a globally signed social web. Bluesky is the most prominent example, but a broader wave of decentralized social applications is gaining momentum.

Building on Cloudflare’s Developer Platform eliminates much of the operational burden typically associated with web applications — no VM management, database scaling, CI pipeline maintenance, or DDoS mitigation. Key services include Workers for global code deployment, KV for distributed caching, D1 as a relational database, and Durable Objects for WebSocket coordination. The entire stack qualifies for Cloudflare’s free tier, and the complete code is available in this GitHub repository.

How Data Flows in the ATProto Ecosystem

The protocol separates concerns into three independently operated components: repositories, relays, and apps. Users interact with apps that write changes to their personal repositories. These changes trigger events that are published to a relay, which broadcasts them across a global event stream. Since repos, relays, and apps are decoupled, any app can subscribe to events it did not originate.

BLOG-2813 image 1

The Role of Identity and Authentication

User identity begins with human-readable handles such as alice.example.com, which must be valid domain names. By leveraging DNS, the protocol offers a global mechanism for resolving account ownership. Handles map to a Decentralized Identifier (DID), which stores the location of a user’s personal data server (PDS). The PDS manages user keys, repositories, and authentication, providing an authoritative source of user data.

BLOG-2813 image 2

The trust model is particularly notable: no component requires blind faith in a single service. DID resolution is verifiable, users select their own PDS, and the client app serves merely as an interface. Every published or fetched record is signed and self-validating, meaning other applications can consume and build upon data without requesting permission or depending on the original service’s backend.

Introducing Statusphere

BLOG-2813 image 3

Statusphere is a minimal demonstration app from the ATProto team, designed for users to post single-emoji updates — the simplest possible social interaction. Its streamlined design makes it an ideal playground for understanding decentralized ATProto mechanics and reworking them for a serverless Cloudflare deployment.

Statusphere's Data Schema

ATProto stores all repository data using Lexicons, a typed schema language similar to JSON-Schema. Statusphere defines a single xyz.statusphere.status record type, authored by the ATProto team:

{
  "type": "record",
  "key": "tid", # timestamp-based id
  "record": {
    "type": "object",
    "required": ["status", "createdAt"],
    "properties": {
      "status": { "type": "string", "maxGraphemes": 1 },
      "createdAt": { "type": "string", "format": "datetime" }
    }
  }
}

The strong typing of Lexicons enables seamless interoperability across diverse applications.

Implementation walkthrough

Statusphere follows ATProto's data flow: handle resolution, authenticated repo access, and real-time event delivery — all running on Cloudflare's serverless stack.

Language and runtime considerations

ATProto's core libraries are TypeScript-first, and Cloudflare Workers offer native TypeScript support, making that the obvious starting point. However, the ATProto TypeScript libraries assume either a backend or browser context. While Workers support Node.js APIs in a serverless environment, the library's reliance on the 'error' redirect handling mode isn't compatible with the edge runtime.

Cloudflare also supports Rust in Workers via WASM cross-compilation. The ATProto Rust crates are under active development but usable; adapting an existing Rust implementation of Statusphere produced a working prototype quickly. The code lives in this GitHub repo. For anyone building ATProto apps on Workers, contributing to the TypeScript libraries to improve serverless runtime support would be valuable — a TypeScript port would be a natural next step.

You can deploy your own instance using the Deploy to Cloudflare button, which clones the repo and provisions KV, D1, and a CI pipeline. Follow the setup steps with default or custom names and it will build and deploy the Worker. Note: the project includes a scheduled component that reads the public event stream — you may want to remove it after experimenting to save resources.

Identity resolution and session handling

Interacting with a user's data starts by resolving their handle to a DID via the record at the _atproto subdomain. For example, the handle inanna.recursion.wtf maps to a TXT record at _atproto.inanna.recursion.wtf with the value did:plc:p2sm7vlwgcbbdjpfy6qajd4g.

The DID is then resolved to a DID Document containing identity metadata, including the user's Personal Data Server (PDS) location. For did:web identifiers this resolution happens directly via DNS; for did:plc identifiers it goes through the Public Ledger of Credentials. Since these mappings rarely change but are read frequently, Statusphere caches them in Cloudflare KV for low-latency global access.

From the DID Document, the app extracts the PDS endpoint — commonly bsky.social, though users can self-host or use alternative providers. The OAuth flow itself follows standard practice: the user authenticates through their PDS, which grants the app permission to act on their behalf with managed signing keys. Session state persists in a secure cookie via tower-sessions, with only an opaque session ID stored client-side; all OAuth state lives in Cloudflare KV.

Reading and writing repo data

With the DID from the session cookie, the app restores the OAuth session and creates an authenticated agent. That agent fetches the user's latest Statusphere post and Bluesky profile in parallel, then renders the homepage with that data.

When a user publishes a new emoji status, the same agent performs a create record operation, adding a new record to their personal repo. The operation returns a URI — the canonical identifier for the record — and the update is written to D1 so it immediately appears in the UI.

Real-time broadcasting with Durable Objects

Each active homepage holds a WebSocket connection to a Durable Object that acts as a lightweight message broker. When idle, the Durable Object hibernates, conserving resources while keeping connections alive. Publishing a new status sends a message to the Durable Object, waking it to broadcast the update to every connected homepage.

A practical scalability note: Durable Objects perform better when sharded across instances. Statusphere deliberately uses a single instance for simplicity. Scaling would involve multiple instances per supported location using location hints to reduce latency and avoid bottlenecks under high concurrency — a pattern that conflicts with the goal of a concise, clonable template for ATProto developers.

Handling external events on serverless infrastructure

Publishing updates within the app is straightforward, but ATProto's federated model means other applications can publish status updates for users. Staying in sync requires listening to ATProto's Jetstream service for live repo events. Traditional servers can hold WebSocket client connections open indefinitely, but Workers can't run forever — so Statusphere needed a different approach.

The solution uses a Cloudflare Cron Trigger. Instead of maintaining a live socket, a scheduled job runs at intervals, reads updates in small batches, and exits. Each invocation loads the last seen cursor from persistent storage, connects to Jetstream filtered by the xyz.statusphere.status collection, and processes events starting from that cursor.

The cursor — a microsecond timestamp marking the last processed message — is stored in the Durable Object's persistent storage, so it can resume exactly where it left off even after a restart. As soon as an event newer than the start time is processed, the WebSocket closes and the Durable Object returns to hibernation.

The tradeoff: updates may lag by up to a minute, but the system remains fully serverless. That's an acceptable trade for early-stage apps and prototypes where minimizing infrastructure complexity outweighs perfect real-time delivery.

Optional upgrade: a lightweight event listener

For true real-time updates, Statusphere offers an optional listener process that bends the serverless model slightly. Instead of polling once a minute, a small process maintains a persistent WebSocket connection to Jetstream, watching for new events in the xyz.statusphere.status collection and pushing them to the Worker immediately. A sketch of this process and the endpoint that handles its updates are in the repo.

The result still isn't a traditional server:

  • No public web exposure
  • No open HTTP ports
  • No persistent database

It's a single-purpose, stateless listener — simple enough to run on a home server while the app grows. For larger deployments, tools like Cloudflare Queues could provide batching and retries, but this lightweight listener works well for small-to-medium applications.

Future directions

Durable Objects currently support hibernation while holding long-lived WebSocket server connections, but not long-lived WebSocket client connections like a Jetstream listener. That limitation is why Statusphere relies on scheduled Cron Triggers and external listeners to stay synced. Adding hibernation support for active WebSocket clients could eliminate these workarounds entirely.

Putting it all together

The resulting application is a complete ATProto service running entirely on Cloudflare's Developer Platform. There are no servers to provision and no traditional operations burden: Worker code executes within roughly 50 ms of most users globally, data persists across KV and D1, and Durable Objects manage WebSocket fan-out and live coordination.

You can deploy the full project directly from the repository using the button below. This clones the statusphere-serverless repo and provisions the necessary Cloudflare resources automatically.

Deploy to Cloudflare

Show us what you built

Once your instance is live, we'd like to see it. Share your deployment in the Cloudflare Developers Discord, or tag @cloudflare.social on Bluesky or @CloudflareDev on X. The full source code is available in the public repository for reference and further experimentation.