{
"$type": "site.standard.document",
"content": {
"$type": "at.markpub.markdown",
"flavor": "gfm",
"renderingRules": "marked",
"text": {
"$type": "at.markpub.text",
"markdown": "# identity\n\nidentity in atproto separates \"who you are\" from \"where you're hosted.\"\n\n## DIDs\n\na DID (Decentralized Identifier) is your permanent identity. it looks like:\n\n```\ndid:plc:xbtmt2zjwlrfegqvch7fboei\n```\n\nthe 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.\n\natproto 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.\n\n`did:web` is also supported, using DNS as the resolution mechanism. this gives you full control but requires maintaining infrastructure.\n\n## handles\n\na handle is the human-readable name:\n\n```\nzzstoatzz.io\npfrazee.com\n```\n\nhandles are DNS-based. you prove ownership by either:\n- adding a DNS TXT record at `_atproto.yourdomain.com`\n- serving a file at `/.well-known/atproto-did`\n\nhandles 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.\n\n## resolution\n\nto find a user:\n\n1. resolve handle → DID (via DNS or well-known)\n2. resolve DID → DID document (via PLC directory)\n3. DID document contains PDS endpoint\n4. query PDS for data\n\n```python\n# simplified resolution flow\nhandle = \"zzstoatzz.io\"\ndid = resolve_handle(handle) # → did:plc:...\ndoc = resolve_did(did) # → {service: [...], alsoKnownAs: [...]}\npds_url = doc[\"service\"][0][\"serviceEndpoint\"]\n```\n\n## caching\n\nDID resolution is expensive (HTTP calls to PLC directory). cache aggressively:\n\n```python\n_did_cache: dict[str, tuple[str, float]] = {}\nDID_CACHE_TTL = 3600 # 1 hour\n\nasync def get_did(handle: str) -> str:\n if handle in _did_cache:\n did, ts = _did_cache[handle]\n if time.time() - ts < DID_CACHE_TTL:\n return did\n did = await resolve_handle(handle)\n _did_cache[handle] = (did, time.time())\n return did\n```\n\nfrom [at-me](https://github.com/zzstoatzz/at-me) - caches DID resolutions with 1-hour TTL.\n\n## PLC operations, from the implementing side\n\nlessons from [zds](https://tangled.org/zat.dev/zds) (zig PDS) creating and\nrotating `did:plc` identities (`src/atproto/plc.zig`):\n\n- the DID itself is `base32lower(sha256(signed genesis op))[:24]` — computed\n on the *signed* operation, with the multibase `b` prefix dropped before\n truncation (`plc.zig:82-86`). both details are easy to get wrong and produce\n a valid-looking, wrong DID.\n- **rotation key order is priority order**: zds places the recovery key first\n in `rotationKeys`, ahead of the PDS's own rotation key, so account recovery\n outranks the host (`plc.zig:55-64`).\n- rotation keys arrive in two encodings — 64-hex secp256k1 or `z`-prefixed\n multikey, discriminated by prefix bytes `0x81 0x26` (secp256k1) vs\n `0x86 0x26` (p256) on the 34-byte decode (`plc.zig:32-45`).\n- signing the genesis op is encode-sign-re-encode: serialize the unsigned\n dag-cbor, sign it, then serialize again with the signature in place.\n\n## why this matters\n\nthe separation of identity (DID) from location (PDS) and presentation (handle) is what enables the \"connected clouds\" model. you can:\n\n- switch PDS providers without losing followers\n- use your own domain as your identity\n- maintain identity even if banned from specific applications\n\nyour identity is yours. hosting is a service you can change.\n"
}
},
"path": "/protocols/atproto/identity",
"publishedAt": "2026-07-08T20:16:04.408Z",
"site": "at://did:plc:xbtmt2zjwlrfegqvch7fboei/site.standard.publication/notes",
"textContent": "identity\n\nidentity in atproto separates \"who you are\" from \"where you're hosted.\"\n\nDIDs\n\na DID (Decentralized Identifier) is your permanent identity. it looks like:\n\ndid:plc:xbtmt2zjwlrfegqvch7fboei\n\n\nthe 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.\n\natproto 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.\n\ndid:web is also supported, using DNS as the resolution mechanism. this gives you full control but requires maintaining infrastructure.\n\nhandles\n\na handle is the human-readable name:\n\nzzstoatzz.io\npfrazee.com\n\n\nhandles are DNS-based. you prove ownership by either:\n\nadding a DNS TXT record at _atproto.yourdomain.com\nserving a file at /.well-known/atproto-did\n\nhandles 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.\n\nresolution\n\nto find a user:\n\nresolve handle → DID (via DNS or well-known)\nresolve DID → DID document (via PLC directory)\nDID document contains PDS endpoint\nquery PDS for data\n# simplified resolution flow\nhandle = \"zzstoatzz.io\"\ndid = resolve_handle(handle) # → did:plc:...\ndoc = resolve_did(did) # → {service: [...], alsoKnownAs: [...]}\npds_url = doc[\"service\"][0][\"serviceEndpoint\"]\n\ncaching\n\nDID resolution is expensive (HTTP calls to PLC directory). cache aggressively:\n\n_did_cache: dict[str, tuple[str, float]] = {}\nDID_CACHE_TTL = 3600 # 1 hour\n\nasync def get_did(handle: str) -> str:\n if handle in _did_cache:\n did, ts = _did_cache[handle]\n if time.time() - ts < DID_CACHE_TTL:\n return did\n did = await resolve_handle(handle)\n _did_cache[handle] = (did, time.time())\n return did\n\n\nfrom at-me - caches DID resolutions with 1-hour TTL.\n\nPLC operations, from the implementing side\n\nlessons from zds (zig PDS) creating and rotating did:plc identities (src/atproto/plc.zig):\n\nthe 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.\nrotation 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).\nrotation 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).\nsigning the genesis op is encode-sign-re-encode: serialize the unsigned dag-cbor, sign it, then serialize again with the signature in place.\nwhy this matters\n\nthe separation of identity (DID) from location (PDS) and presentation (handle) is what enables the \"connected clouds\" model. you can:\n\nswitch PDS providers without losing followers\nuse your own domain as your identity\nmaintain identity even if banned from specific applications\n\nyour identity is yours. hosting is a service you can change.",
"title": "identity",
"updatedAt": "2026-07-21T19:27:28.082Z"
}