If you’ve spent any time in the decentralized social web, you’ve probably seen links like this:

at://ruuuuu.de/app.bsky.feed.post/3lzy2ji4nms2z

These URIs look a bit like URLs, but they don't contain a server name or an https:// scheme. So where do they actually point, and how does a client fetch the JSON behind them?

Users Are the Authority

In a traditional URL, the authority section—the part after https://—identifies the machine hosting the resource. The creator of the content is either absent or buried in the path.

The at:// protocol flips this relationship. In an at:// URI, the authority is the user or entity that created the data, not the server that currently stores it. The physical hosting location can change over time and is intentionally left out of the URI. Finding that host takes a few intermediate steps.

Resolving an at:// URI is a three-stage process:

  1. Resolve the handle to an identity.
  2. Resolve the identity to a hosting server.
  3. Request the JSON from that server.

Handles vs. Identities

The ruuuuu.de part of the URI above is a handle. Handles are human-readable and can change; the person behind @ruuuuu.de might decide to use a different domain next year. If links broke every time someone changed handles, the network would be unusable.

That’s why the network separates handles from immutable identities—formally, DIDs. A DID looks like did:web:iam.ruuuuu.de or did:plc:fpruhuo22xkm5o7ttr2ktxdo. It never changes for the life of an account.

To find the DID for a handle, you can query the DNS TXT record at _atproto.<handle>, looking for a did=... value. Alternatively, you can make an HTTPS GET request to https://<handle>/.well-known/atproto-did. Both methods work for different handle types; for a subdomain like barackobama.bsky.social, the DNS lookup may return nothing, and the HTTPS fallback is required.

This gives you the DID that the handle claims to own. The mapping is bidirectional: you still need to confirm that the DID’s owner agrees to use that handle.

DID Documents Are Passports

Both verification and hosting discovery come from the DID Document, a JSON file describing the identity. It contains three critical pieces of information:

  • How to refer to the identity. The alsoKnownAs field lists the handles the identity controls.
  • How to verify data integrity. The publicKeyMultibase field holds the public key used to sign the identity’s records.
  • Where the data lives. The serviceEndpoint field points to the actual server hosting the data.

The path to this document depends on the DID method. Two are supported by AT Protocol today: did:web and did:plc.

did:web

For a did:web DID, the host is derived from the DID itself. You strip the did:web: prefix, append /.well-known/did.json, and fetch it over HTTPS.

This approach is simple but has a notable vulnerability: the identity is tied to a domain name. If the domain registration lapses or is lost, the owner loses control of the DID Document—and with it, the entire identity. The domain owner is the identity owner, by definition.

did:plc

The did:plc method avoids the domain risk. Instead of being tied to a web host, the DID Document is stored in a registry served at https://plc.directory. The DID itself is the hash of the initial document; every subsequent update is signed with the previous version’s hash, creating a verifiable chain of history.

This means a did:plc identity can’t be lost if a domain expires. The tradeoff is that the registry operator has some power: it could refuse requests to update a DID Document or withhold serving information. To address these concerns, Bluesky is moving the PLC registry to an independent legal entity in Switzerland, and the broader AT community is exploring alternatives.

Fetching the JSON

Once you have the DID Document, the serviceEndpoint reveals the physical server. To grab the actual record, you send an HTTPS GET to that server using the com.atproto.repo.getRecord API endpoint, passing the pieces of the at:// URI as parameters.

For instance, after resolving the handle ruuuuu.de to the DID did:web:iam.ruuuuu.de, its DID Document points to https://blacksky.app as the host. Hitting that server’s getRecord endpoint with the URI’s collection and record key returns the JSON: a post with fields like text, langs, and $type indicating the data format.

Note that the JSON you get back is pure data—not a web page. The $type field declares the format, and the app.bsky.* prefix hints that the bsky.app application knows how to consume it, but other apps may use the same format.

What’s Really Happening

When you see a at:// URI with a handle, think of it as a human-friendly link that might break if someone changes their handle. Applications that store these URIs for the long term should first resolve the handle to a DID and store that canonical form. A URI like at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/sh.tangled.feed.star/3m23ddgjpgn22 is a true permalink, immune to handle changes and server migrations.

Resolving a single URI on demand is possible, but doing it for every request is wasteful. In practice, applications often rely on resolution caches or SDKs that handle these lookups. Many large-scale consumers of AT data skip direct resolution altogether: they subscribe to a websocket feed and aggregate records into a local database, using at:// URIs purely as unique identifiers.

Under the hood, AT Protocol is an abstraction over HTTP, DNS, and JSON. By placing the creator in the authority position, separating identity from hosting, and making data portable, it ensures that links point to the data itself—not to the app that happens to display it.