Glossary

The Two Records Behind a Bluesky Custom Domain Handle

Taras Shynkarenko
Taras Shynkarenko
Updated: 10 min read
The two records behind a Bluesky custom domain handleThe two records behind a Bluesky custom domain handle

TL;DR, Quick Answer

10 min read

A domain becomes your Bluesky handle once you prove you control it, either with a DNS TXT record on the _atproto subdomain whose value is did=did:plc:yourdid, or with a plain text HTTPS response at /.well-known/atproto-did containing the bare DID. The AT Protocol handle spec caps handles at 253 ASCII characters, 244 in practice, and disallows the .alt, .arpa, .example, .internal, .invalid, .local, .localhost and .onion TLDs. A handle only counts once the DID document links back to it.

What is a Bluesky custom domain handle?

A domain you own becomes your Bluesky custom domain handle once you publish one record proving you control it, either a DNS TXT record on the _atproto subdomain or a plain text file served at /.well-known/atproto-did. Bluesky runs on the AT Protocol, and its handle specification decides whether your record works. Help articles describe the buttons in the app. The spec describes what the network checks afterwards.

Read this before you touch DNS: "Handles are mutable and human-friendly account usernames, in the form of a DNS hostname." Your handle is a hostname, which is why mozilla.org works and mozilla on its own does not: "'bare' top-level domains are not allowed as handles, even if valid 'hostnames' and 'DNS names.'"

Behind every handle sits a DID, one of "the long-term persistent identifiers for accounts in atproto." You paste your DID into whichever record you choose, so that is the part you need here. The rest of what a DID is belongs on its own page.

Which record do you publish, and what goes in it?

Publish one of these two, with your own DID in place of the example.

DNS TXT methodHTTPS well-known method
Location_atproto.example.comhttps://example.com/.well-known/atproto-did
MechanismTXT recordHTTP GET response
Valuedid=did:plc:ewvi7nxzyoun6zhxrhs64oizdid:plc:ewvi7nxzyoun6zhxrhs64oiz
did= prefixRequiredMust not be present
Content typeNot applicabletext/plain
RedirectsNot applicableAllowed, "up to a reasonable number of redirect hops"
Spec status"recommended and preferred" for individualsIntended for services registering handles in bulk

The prefix is the most common paste error, and neither method warns you. DNS resolvers ignore "TXT records with values not starting with did=." Over HTTPS that same prefix breaks the body, which the spec wants as "the DID... with no prefix or wrapper formatting."

The DNS record for a handle at the apex of example.com:

Type:  TXT
Name:  _atproto
Value: did=did:plc:ewvi7nxzyoun6zhxrhs64oiz
TTL:   300

For a subdomain handle such as alice.example.com, the name is _atproto.alice. Most DNS panels append the zone for you, so typing the full _atproto.alice.example.com produces _atproto.alice.example.com.example.com and silent failure.

Only one record may exist: "If multiple valid records with different DIDs are present, resolution should fail." An old record from a previous account breaks the new one.

The HTTPS response the spec asks for, reproduced exactly:

HTTP/1.1 200 OK
Content-Length: 33
Content-Type: text/plain
Date: Wed, 14 Jun 2023 00:47:21 GMT

did:plc:ewvi7nxzyoun6zhxrhs64oiz

One paragraph later, in the same section, the spec relaxes its own requirement: "The response Content-Type header does not need to be strictly verified." Serve text/plain anyway, since you cannot know which resolver reads which line.

Your DID comes from the app. Bluesky's tutorial gives the path as "Settings," then "Account," then "Handle," and shows the record host as _atproto with the value "did=did:plc:[your value here]".

What are the handle syntax rules?

Seven rules, and a handle has to satisfy all of them:

  1. The whole handle is ASCII only and at most 253 characters.
  2. Segments are separated by ASCII periods, and there must be at least two of them.
  3. No leading or trailing periods, and no trailing dot DNS syntax.
  4. Each segment is 1 to 63 characters, drawn from ASCII letters a-z, digits 0-9, and hyphens -.
  5. A segment cannot start or end with a hyphen.
  6. The last segment, the top level domain, cannot start with a digit.
  7. Handles are case insensitive and normalize to lowercase.

On the last rule: "the handle input string BlueskyWeb.xyz should be normalized, stored, and displayed as blueskyweb.xyz." If your brand is capitalised, the handle will not be.

The reference regular expression from the spec:

/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/

253 is the headline number and not the one to design around. Buried in the implementation guidelines is a smaller one: "handles should be limited to at most 244 characters... because DNS verification works with the prefix _atproto., which adds 9 characters." Anything between 245 and 253 characters is legal by the syntax section and unresolvable by DNS, and the escape hatch sits in a different section: "The HTTPS method will work for such handles."

AdaptlyPost
AdaptlyPost

Start 7-Day FREE Trial

All-platform analytics

Social Inbox

AI-powered assistant

Examples the spec marks as valid, alongside the ones it rejects:

Valid syntaxInvalid syntaxWhy the second column fails
jay.bsky.socialjo@hn.test@ is not an allowed character
8.cnjohn..testEmpty segment between periods
a.cojohn.0TLD starts with a digit
XX.LCS.MIT.EDUorgOnly one segment
xn--notarealidn.comname.org.Trailing period
name.t--txn--bcher-.tldSegment ends with a hyphen

Internationalised domains work in encoded form only: "Such handles must be stored and transmitted in encoded ASCII form."

The handle length budget
Maximum by spec253 characters
Safe for DNS TXT244 characters
_atproto prefix9 characters
The DNS TXT method eats 9 of the 253 allowed characters before your domain even starts, which is why the safe ceiling drops to 244.

Which TLDs can never be a Bluesky handle?

Eight top level domains are disallowed outright. From the spec: "the initial list of disallowed TLDs includes: .alt, .arpa, .example, .internal, .invalid, .local, .localhost, .onion."

Their failure mode is deliberate and odd. Reserved TLDs "should not fail syntax validation... but they must immediately fail any attempt at registration, resolution, etc." So laptop.local and blah.arpa pass a regex check and fail forever at resolution. A validator running only the regex calls the handle fine until the network refuses it.

Three items carry extra conditions:

  • .onion is blocked for a stated reason rather than a permanent one: "Resolution of handles via Tor would require ecosystem-wide support, so they are currently disallowed." The future changes section adds that ".onion handles would be allowed at some point in the future."
  • .invalid is reserved for one sentinel value, handle.invalid, which the API returns "to indicate that there is no bi-directionally valid handle for the given DID."
  • .test is not on the disallowed list and is not usable either. It "may be used in atproto development, but should fail in real-world environments."

Note the word "initial." The list is not framed as final, and the spec links Wikipedia's reserved domains section rather than keeping its own registry. Eight names are what the protocol commits to in writing, and the boundary past them is drawn on a page nobody in the AT Protocol project controls.

A developer runs a terminal command to look up a domain's DNS records.

How do you resolve a handle to its DID?

Run it against a real account and both halves of the check happen in front of you. Every value below was resolved on 10 September 2026.

Start with the DNS record for bsky.app, Bluesky's own handle:

dig +short TXT _atproto.bsky.app
"did=did:plc:z72i7hdynmk6r22z27h6tvur"

Strip the did= prefix and you have the DID. The protocol's own endpoint, which clients call instead of doing DNS themselves, returns the same answer:

curl -s "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=bsky.app"
{ "did": "did:plc:z72i7hdynmk6r22z27h6tvur" }

Now fetch the DID document, which is the other half of the check:

curl -s https://plc.directory/did:plc:z72i7hdynmk6r22z27h6tvur
{
  "@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:z72i7hdynmk6r22z27h6tvur",
  "alsoKnownAs": ["at://bsky.app"],
  "verificationMethod": [
    {
      "id": "did:plc:z72i7hdynmk6r22z27h6tvur#atproto",
      "type": "Multikey",
      "controller": "did:plc:z72i7hdynmk6r22z27h6tvur",
      "publicKeyMultibase": "zQ3shQo6TF2moaqMTrUZEM1jeuYRQXeHEx4evX9751y2qPqRA"
    }
  ],
  "service": [
    {
      "id": "#atproto_pds",
      "type": "AtprotoPersonalDataServer",
      "serviceEndpoint": "https://puffball.us-east.host.bsky.network"
    }
  ]
}

The field that decides everything is alsoKnownAs, holding at://bsky.app. The handle pointed at the DID, and the DID document points back. The spec requires both directions: "The link between handle and DID must be confirmed bidirectionally, otherwise anybody could create handle aliases for third-party accounts." Publishing a TXT record naming someone else's DID gets you nothing, because their DID document does not name your domain.

When a handle does not resolve, the endpoint answers with one string:

{ "error": "InvalidRequest", "message": "Unable to resolve handle" }

That arrives with HTTP 400, and it is the same message for a missing record, a valid handle on a reserved TLD, and a domain that does not exist. Diagnose with dig and curl rather than reading the API response.

Four handles resolved on that date, all confirmed bidirectionally against plc.directory:

AdaptlyPost
AdaptlyPost

Start 7-Day FREE Trial

All-platform analytics

Social Inbox

AI-powered assistant

HandleDIDMethod that answered
bsky.appdid:plc:z72i7hdynmk6r22z27h6tvurDNS TXT
mozilla.orgdid:plc:jxrrsbtaptoynkhm2tdvxohgDNS TXT
theverge.comdid:plc:7exlcsle4mjfhu3wnhcgizz6DNS TXT
jay.bsky.socialdid:plc:mfm3grjeffxyfuxj3uiisfuzHTTPS well-known

That split is the spec's design working as written. Organisations with a DNS panel use the TXT record. The handle answering over HTTPS is a bsky.social subdomain, the "large-scale web services which may not have the infrastructure to automate the registration of thousands or millions of DNS TXT records" case the method was built for.

Requesting https://mozilla.org/.well-known/atproto-did returns a 404 HTML page, and theverge.com does the same. Both handles resolve fine. Only one method has to answer, and a 404 on the path you did not use costs you nothing.

Where do custom domain handles break?

SymptomCauseFix
Handle rejected, DNS record looks rightRecord name became _atproto.example.com.example.com because the panel appends the zoneEnter _atproto alone in the name field
Resolution fails after a renameAn old TXT record with a different DID is still publishedDelete it, since "If multiple valid records with different DIDs are present, resolution should fail"
Handle over 244 characters fails_atproto. adds 9 characters and pushes the query past 253Use the HTTPS method, which the spec names for this case
Well-known path returns your app's HTMLFramework catch-all route answers before the static fileServe the path before the catch-all, with Content-Type: text/plain
Two methods disagreeBoth a TXT record and a well-known file exist, naming different DIDsThe spec says "the DNS TXT result should be preferred"; remove the stale one anyway
Handle shows as handle.invalidThe handle stopped resolving after it was already verifiedRepublish the record; the spec warns a PDS "may prevent repo mutation" while an account sits in this state
Change not visible yetCaching, since services "cache handle resolution results internally, up to some lifetime"Wait and re-resolve, and set a short TTL before the change

Redirects deserve a note, because other well-known specifications ban them and this one does not: "HTTP redirects (eg, 301, 302) are allowed, up to a reasonable number of redirect hops." An apex to www redirect is fine. A redirect landing on a 404 is not, which is what happens when the apex forwards to www and the file was never deployed there.

Someone checks a queue of scheduled social posts on their phone.

What happens to your scheduled posts when the handle changes?

Nothing, and the reason is structural. Your account is the DID, which the protocol calls persistent, while the handle is a mutable label resolved fresh whenever a client needs it. Moving from you.bsky.social to yourbrand.com leaves the DID untouched, so a queue built in a Bluesky post scheduler survives the switch, and so do the numbers in a Bluesky analytics tool. That is protocol behaviour, not something a publishing tool grants or withholds.

Two things do change. Posts you already published now display under the new handle, because clients render the handle they resolve rather than one frozen at publish time. And any copy naming your old handle is now wrong, which bites hardest when you publish the same post across several networks at once and the Bluesky handle sits in a caption shared with X, LinkedIn and Threads. Search your queued drafts for the old string before the switch. Five minutes in a content calendar, against a month of published posts to fix afterwards.

Frequently Asked Questions

Do I need both the DNS record and the well-known file?

No. One is enough, and the spec calls the DNS TXT method "the recommended and preferred resolution method for individual handle configuration." If you publish both and they name different DIDs, resolvers prefer the DNS answer. Delete the stale one rather than leaving the conflict in place.

Why does my TXT record need did= when the HTTPS file does not?

The two methods parse differently. DNS follows RFC-1464 for storing attributes in TXT records, so the value is a key and a value, and resolvers ignore "TXT records with values not starting with did=." The HTTPS method returns "the DID as the HTTP body with no prefix or wrapper formatting," so the same prefix makes the body unparseable.

Can I use a .local or .internal domain for testing?

No. Both are on the disallowed list, and reserved TLDs "must immediately fail any attempt at registration, resolution, etc." even though they pass syntax validation. The .test TLD is the one meant for development, and the spec still expects it to "fail in real-world environments."

How long is a Bluesky handle allowed to be?

253 ASCII characters by the syntax rules, 244 in practice. The _atproto. prefix used for DNS verification "adds 9 characters, and that overall name needs to be valid." Above 244, only the HTTPS method resolves.

What is handle.invalid and why am I seeing it?

It is the value the API returns when a DID has no working handle, used "to indicate that there is no bi-directionally valid handle for the given DID." It appears after a handle that once resolved stops resolving. Republishing the correct record clears it.

No. Bluesky's tutorial states that "Any tags or mentions with your old handle will still point to your account," because mentions store the DID rather than the handle text. Since December 2024 your previous .bsky.social username is also reserved for you rather than released.

Why doesn't publishing someone else's DID in my TXT record work?

The protocol checks the link in both directions. Your record can name any DID, but the DID document also has to point back to your domain in its alsoKnownAs field, using an at:// URI. Bluesky's own DID document lists at://bsky.app for exactly that reason, and without a matching reverse link anybody could otherwise create handle aliases for third-party accounts, so a TXT record naming someone else's DID resolves to nothing.

Can I start posting under my custom domain handle right after publishing the record?

Not always right away. Services cache handle resolution results internally for a while, so a stale answer can linger even after the record itself is correct. The TTL in the example DNS record above is 300 seconds, and setting a short TTL before the change and re-resolving afterward is what cuts that wait down.

What happens to my old .bsky.social username after I switch to a custom domain?

It stays reserved for you. Since December 2024, Bluesky holds your previous .bsky.social handle instead of releasing it back into the pool once you move to a custom domain, so nobody else can claim it.

AdaptlyPost
AdaptlyPost

Start 7-Day FREE Trial

All-platform analytics

Social Inbox

AI-powered assistant

Will a redirect from my apex domain to www break handle verification?

Not by itself. The spec allows HTTP redirects up to a reasonable number of hops, so an apex-to-www redirect for the well-known path is fine on its own. It only breaks verification if the redirect lands somewhere the file was never deployed, since a redirect ending in a 404 does not resolve.

Was This Article Helpful?

Let us know what you think!

See us more often in Google

One click marks AdaptlyPost as a preferred source, so our articles sit higher in your Top Stories, AI Mode, and AI Overviews.

Before you go...

AdaptlyPost

AdaptlyPost

Schedule your content across all platforms

Manage all your social media accounts in one place with AdaptlyPost.

All-platform analytics

Social Inbox

AI-powered assistant

Related Glossary Terms

Related Articles