Skip to content

Apex handle

Overview

How to give an account the apex handle example.com instead of a something.example.com subdomain.

The PDS only auto-accepts handles under its configured domain (the .example.com subdomains), and the apex is also the PDS service identity (did:web:example.com). So the handle is applied in two stages. Create the account under a subdomain, then switch it to the apex once handle verification is in place.

Requirements

  • DNS control over example.com to add a TXT record under _atproto.
  • The account's DID, which pdsadmin lists once the account exists.
  • An invite code if you create the account through the XRPC API rather than pdsadmin. Generate one with pdsadmin create-invite-code.

Handle verification

AT Protocol resolves a handle to a DID by one of two methods, and the PDS accepts a handle once either one points at the account DID.

  1. DNS: a TXT record at _atproto.<handle> whose value is did=<did>.
  2. HTTPS: a file at https://<handle>/.well-known/atproto-did containing the DID as plain text.

Prefer DNS for the apex. The HTTPS method would place a file under the same origin the PDS serves, which risks colliding with the PDS routes.

The record for example.com:

_atproto.example.com.  TXT  "did=did:plc:XXXXXXXXXXXXXXXXXXXXXXXX"

Steps

Create the account with a bootstrap subdomain handle. pdsadmin prints a generated password once:

docker exec bluesky-pds \
    pdsadmin account create <email> bootstrap.example.com

Find the account DID:

docker exec bluesky-pds pdsadmin account list

Add the _atproto.example.com TXT record with that DID at your DNS provider, then wait for it to propagate. Confirm it resolves before continuing:

dig +short TXT _atproto.example.com

Authenticate as the account to get an accessJwt. updateHandle fails if the DNS record is not yet visible to the PDS, so run this only after the record resolves:

docker exec bluesky-pds curl --silent --request POST \
    http://localhost:3000/xrpc/com.atproto.server.createSession \
    --header 'Content-Type: application/json' \
    --data '{"identifier":"bootstrap.example.com","password":"<account-password>"}'

Switch the handle to the apex, passing the accessJwt:

docker exec bluesky-pds curl --silent --request POST \
    http://localhost:3000/xrpc/com.atproto.identity.updateHandle \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <accessJwt>' \
    --data '{"handle":"example.com"}'

Verify the apex now resolves to the account DID:

docker exec bluesky-pds curl --silent \
    'http://localhost:3000/xrpc/com.atproto.identity.resolveHandle?handle=example.com'