identity
identity
identity in atproto separates "who you are" from "where you're hosted."
DIDs
a DID (Decentralized Identifier) is your permanent identity. it looks like:
did:plc:xbtmt2zjwlrfegqvch7fboei
the DID never changes, even if you move to a different PDS. this is what makes account migration possible - your identity isn't tied to your host.
atproto primarily uses did:plc, where the PLC Directory (plc.directory) maintains a mapping from DIDs to their current metadata: signing keys, PDS location, and associated handles.
did:web is also supported, using DNS as the resolution mechanism. this gives you full control but requires maintaining infrastructure.
handles
a handle is the human-readable name:
zzstoatzz.io pfrazee.com
handles are DNS-based. you prove ownership by either:
adding a DNS TXT record at _atproto.yourdomain.com serving a file at /.well-known/atproto-did
handles can change. they're aliases to DIDs, not identities themselves. if you lose a domain, you lose the handle but keep your DID and all your data.
resolution
to find a user:
resolve handle → DID (via DNS or well-known) resolve DID → DID document (via PLC directory) DID document contains PDS endpoint query PDS for data
simplified resolution flow
handle = "zzstoatzz.io" did = resolve_handle(handle) # → did:plc:... doc = resolve_did(did) # → {service: [...], alsoKnownAs: [...]} pds_url = doc["service"][0]["serviceEndpoint"]
caching
DID resolution is expensive (HTTP calls to PLC directory). cache aggressively:
_did_cache: dict[str, tuple[str, float]] = {} DID_CACHE_TTL = 3600 # 1 hour
async def get_did(handle: str) -> str: if handle in _did_cache: did, ts = _did_cache[handle] if time.time() - ts < DID_CACHE_TTL: return did did = await resolve_handle(handle) _did_cache[handle] = (did, time.time()) return did
from at-me - caches DID resolutions with 1-hour TTL.
PLC operations, from the implementing side
lessons from zds (zig PDS) creating and rotating did:plc identities (src/atproto/plc.zig):
the DID itself is base32lower(sha256(signed genesis op))[:24] — computed on the signed operation, with the multibase b prefix dropped before truncation (plc.zig:82-86). both details are easy to get wrong and produce a valid-looking, wrong DID. rotation key order is priority order: zds places the recovery key first in rotationKeys, ahead of the PDS's own rotation key, so account recovery outranks the host (plc.zig:55-64). rotation keys arrive in two encodings — 64-hex secp256k1 or z-prefixed multikey, discriminated by prefix bytes 0x81 0x26 (secp256k1) vs 0x86 0x26 (p256) on the 34-byte decode (plc.zig:32-45). signing the genesis op is encode-sign-re-encode: serialize the unsigned dag-cbor, sign it, then serialize again with the signature in place. why this matters
the separation of identity (DID) from location (PDS) and presentation (handle) is what enables the "connected clouds" model. you can:
switch PDS providers without losing followers use your own domain as your identity maintain identity even if banned from specific applications
your identity is yours. hosting is a service you can change.
Discussion in the ATmosphere