Documentation Index Fetch the complete documentation index at: https://mintlify.com/bluesky-social/atproto/llms.txt
Use this file to discover all available pages before exploring further.
What is Decentralized Identity?
AT Protocol uses a dual identity system combining cryptographic DIDs (Decentralized Identifiers) with human-readable handles . This allows users to have both a permanent, secure identity and a friendly display name.
Identity Components
DIDs (Decentralized Identifiers)
DIDs are the permanent, cryptographic identifiers for users in AT Protocol:
Format : did:method:identifier
Example : did:plc:ewvi7nxzyoun6zhxrhs64oiz
Permanent : DIDs don’t change, even if you switch servers
Self-sovereign : You control your DID through cryptographic keys
Handles
Handles are human-readable names that map to DIDs:
Format : Domain name (e.g., alice.bsky.social, bob.com)
Changeable : Can be updated without losing your identity
Verifiable : Proven through DNS or HTTPS
Portable : Can use your own domain
Why Two Identifiers?
DIDs provide permanence and cryptographic security
Handles provide usability and memorability
Together they enable both security and user-friendliness
Resolving Handles
The @atproto/identity package provides handle resolution:
import { HandleResolver } from '@atproto/identity'
const resolver = new HandleResolver ({
timeout: 3000 ,
backupNameservers: [ '8.8.8.8' , '1.1.1.1' ]
})
// Resolve handle to DID
const handle = 'atproto.com'
const did = await resolver . resolve ( handle )
console . log ( did ) // 'did:plc:ewvi7nxzyoun6zhxrhs64oiz'
Handle resolution tries two methods:
1. DNS TXT Record
The preferred method using DNS:
# Query DNS TXT record
dig _atproto.alice.bsky.social TXT
# Returns:
_atproto.alice.bsky.social. 300 IN TXT "did=did:plc:abc123"
2. HTTPS Well-Known
Fallback method using HTTPS:
# HTTP request
GET https://alice.bsky.social/.well-known/atproto-did
# Returns:
did:plc:abc123
DID Resolution
DIDs resolve to DID Documents containing service endpoints and public keys:
import { DidResolver } from '@atproto/identity'
const resolver = new DidResolver ({
timeout: 3000 ,
plcUrl: 'https://plc.directory'
})
const did = 'did:plc:ewvi7nxzyoun6zhxrhs64oiz'
const doc = await resolver . resolve ( did )
console . log ( doc )
Example DID Document:
{
"@context" : [
"https://www.w3.org/ns/did/v1" ,
"https://w3id.org/security/multikey/v1" ,
"https://w3id.org/security/suites/secp256k1-2019/v1"
],
"id" : "did:plc:ewvi7nxzyoun6zhxrhs64oiz" ,
"alsoKnownAs" : [ "at://atproto.com" ],
"verificationMethod" : [
{
"id" : "did:plc:ewvi7nxzyoun6zhxrhs64oiz#atproto" ,
"type" : "Multikey" ,
"controller" : "did:plc:ewvi7nxzyoun6zhxrhs64oiz" ,
"publicKeyMultibase" : "zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF"
}
],
"service" : [
{
"id" : "#atproto_pds" ,
"type" : "AtprotoPersonalDataServer" ,
"serviceEndpoint" : "https://morel.us-east.host.bsky.network"
}
]
}
DID Methods
AT Protocol supports two DID methods:
did:plc
PLC (Placeholder) is the primary method used in AT Protocol:
import { DidPlcResolver } from '@atproto/identity'
const resolver = new DidPlcResolver ( 'https://plc.directory' , 3000 )
const doc = await resolver . resolve ( 'did:plc:ewvi7nxzyoun6zhxrhs64oiz' )
Characteristics:
Centralized directory with plans for decentralization
Fast resolution
Supports key rotation and service updates
Used by default in Bluesky
did:web
Web DIDs use domain names:
import { DidWebResolver } from '@atproto/identity'
const resolver = new DidWebResolver ( 3000 )
const doc = await resolver . resolve ( 'did:web:example.com' )
Characteristics:
Fully decentralized
Resolution via HTTPS to https://example.com/.well-known/did.json
Relies on domain ownership
Slower resolution than PLC
The identity package provides helpers to extract AT Protocol-specific data from DID documents:
import { DidResolver } from '@atproto/identity'
const resolver = new DidResolver ({
plcUrl: 'https://plc.directory'
})
const did = 'did:plc:ewvi7nxzyoun6zhxrhs64oiz'
// Get AT Protocol-specific data
const data = await resolver . resolveAtprotoData ( did )
console . log ( data )
// {
// did: 'did:plc:ewvi7nxzyoun6zhxrhs64oiz',
// signingKey: 'did:plc:ewvi7nxzyoun6zhxrhs64oiz#atproto',
// handle: 'atproto.com',
// pds: 'https://morel.us-east.host.bsky.network'
// }
Identity Verification Flow
Verify that a handle matches its DID document:
import { DidResolver , HandleResolver } from '@atproto/identity'
const didRes = new DidResolver ({})
const hdlRes = new HandleResolver ({})
const handle = 'atproto.com'
// Step 1: Resolve handle to DID
const did = await hdlRes . resolve ( handle )
if ( ! did ) {
throw new Error ( 'Handle does not resolve to a DID' )
}
console . log ( `Handle resolves to: ${ did } ` )
// Step 2: Resolve DID to document
const doc = await didRes . resolve ( did )
// Step 3: Extract AT Protocol data
const data = await didRes . resolveAtprotoData ( did )
// Step 4: Verify handle matches
if ( data . handle !== handle ) {
throw new Error ( 'Handle mismatch! Potential impersonation.' )
}
console . log ( '✓ Identity verified' )
console . log ( 'PDS:' , data . pds )
console . log ( 'Signing Key:' , data . signingKey )
Caching
DID resolution can be expensive, so caching is important:
import { DidResolver , MemoryCache } from '@atproto/identity'
const cache = new MemoryCache ()
const resolver = new DidResolver ({
didCache: cache ,
plcUrl: 'https://plc.directory'
})
// First resolution - hits network
const doc1 = await resolver . resolve ( did )
// Second resolution - uses cache
const doc2 = await resolver . resolve ( did )
// Force refresh from network
const doc3 = await resolver . resolve ( did , true )
Cache Interface:
interface DidCache {
cacheDid (
did : string ,
doc : DidDocument ,
prevResult ?: CacheResult
) : Promise < void >
checkCache ( did : string ) : Promise < CacheResult | null >
refreshCache (
did : string ,
getDoc : () => Promise < DidDocument | null >,
prevResult ?: CacheResult
) : Promise < void >
clearEntry ( did : string ) : Promise < void >
clear () : Promise < void >
}
Using Your Own Domain
One of AT Protocol’s key features is the ability to use your own domain as your handle:
Option 1: DNS TXT Record
Add a TXT record to your domain:
_atproto.yourdomain.com IN TXT "did=did:plc:your-did-here"
Option 2: HTTPS Well-Known
Create a file at https://yourdomain.com/.well-known/atproto-did:
Then update your handle in your client application to yourdomain.com.
Identity Portability
The separation of DID and handle enables powerful portability:
// You can change your handle...
await agent . com . atproto . identity . updateHandle ({
handle: 'alice.custom-domain.com'
})
// ...without losing your:
// - DID (permanent identifier)
// - Content (posts, likes, follows)
// - Social graph (followers follow your DID)
// - Reputation (cryptographically signed history)
Best Practices
DID resolution involves network requests. Always use caching in production to reduce latency and load.
Verify handle-to-DID consistency
Always verify that a resolved DID’s document contains the expected handle in alsoKnownAs to prevent impersonation.
Handle resolution failures gracefully
Handle resolution can fail due to DNS issues or misconfiguration. Always have fallback logic.
Set reasonable timeouts for both handle and DID resolution to prevent hanging requests.
Error Handling
import {
PoorlyFormattedDidError ,
UnsupportedDidMethodError
} from '@atproto/identity'
try {
const doc = await resolver . resolve ( 'did:invalid:123' )
} catch ( error ) {
if ( error instanceof PoorlyFormattedDidError ) {
console . error ( 'Invalid DID format' )
} else if ( error instanceof UnsupportedDidMethodError ) {
console . error ( 'DID method not supported' )
} else {
console . error ( 'Resolution failed:' , error )
}
}
Additional Resources
@atproto/identity Package NPM package documentation
DID Specification W3C DID specification
PLC Directory Public PLC DID directory
Identity Spec AT Protocol identity specification