pizza baking in a home oven
pizza baking in a home oven

It's time to tackle one of the most unfortunate warts in the atproto specifications: that the AT URI syntax, as currently specified, is not a valid URI or URL. We put DIDs in the URI "authority" place (just after the at://), and they conflict with the host/port hierarchal URI syntax components specified in RFC 3986.

The core issue was surfaced in 2025 (by @mcc in this github issue), and is coming to a head now for two reasons. First, more folks are trying to use AT URIs in places expecting a well-formed URI (or URL), such as in HTML <link> tags. And second, the ATP Working Group at the IETF has a deliverable of standardizing the AT URI scheme with a permanent registration.

The purpose of this post is give some background on the situation, then talk through some ways we could try and fix it. Unfortunately, I don't think there are any great options on the table: any path the ecosystem takes is going to involve some developer pain and disruption.

I care a lot about this because I think AT URIs are quietly one of the best and most important features of the protocol! Putting an individual account identifier in the authority section, instead of a provider hostname, is both concretely and symbolically one of the Big Ideas of atproto: accounts are the ultimate authority for their content, and can seamlessly move between hosting providers. Accounts are in the driver seat, and resolve to a current network location. That aspect isn't going to change, and if we can resolve this syntax mess then folks can go nuts and start using AT URIs all over the place.

What Went Wrong?

To ground things a bit, here is an example AT URI pointing at a specific record, with a DID in the authority place:

at://did:plc:vwzwgnygau7ed7b7wt5ux7y2/app.bsky.feed.post/3k5nobkf2w72g

And the generic syntax:

at:// <authority> / <collection> / <record-key>

The at: URI schema was originally designed in 2022, and was provisionally registered in 2023. We knew that putting a DID in the authority part could cause compatibility issues, and would mean the syntax wasn't a valid web URL under the WHATWG specification. But we (incorrectly) thought that it still complied with the URI syntax, as standardized in RFC-3986.

Designing AT Protocol has always been a group effort, both inside and outside Bluesky, but I feel a personal responsibility for this particular syntax mess. I spent a fair amount of time in spring 2023 reviewing and "hardening" the written specifications to catch issues exactly like this.

The root of the issue came from motivated reading of RFC-3986, and in particular Section 1.2.3:

For some URI schemes, the visible hierarchy is limited to the scheme itself: everything after the scheme component delimiter (":") is considered opaque to URI processing.

At the time I interpreted this to mean that the underlying principle was that a URI was the scheme name and then colon character. Everything after that, as described in section 3.2, 3.3, and on, seemed like optional "hierarchal" syntax, which could be ignored. There are plenty of valid URI schemes that do this, like email (mailto:John.Doe@example.com), usenet (news:comp.infosystems.www.servers.unix).

Further guidance for URIs which are intended for permanent IANA registration (which we want for the AT URI scheme) are described in RFC-7595 Section 3.2, which says:

Schemes SHOULD avoid improper use of "//". The use of double slashes in the first part of a URI is not a stylistic indicator that what follows is a URI: double slashes are intended for use ONLY when the syntax of the <scheme-specific-part> contains a hierarchical structure. In URIs from such schemes, the use of double slashes indicates that what follows is the top hierarchical element for a naming authority (Section 3.2 of RFC 3986 has more details). Schemes that do not contain a conformant hierarchical structure in their <scheme-specific-part> SHOULD NOT use double slashes following the "<scheme>:" string.

That makes it clear what the best practice is, though i'll note that these are "SHOULD" not "MUST".

Overall, it seems clear that the generic/hierarchal syntax restrictions are intended to be required when // is used. The formal ABNF grammar in RFC-3986 Appendix A requires it. The WHATWG URL syntax leaves no ambiguity about this. And on a pragmatic level, the actual strings fail to parse with most real-world URL and URI libraries.

Why this motivated reasoning in the first place? Because we love the current simple syntax. It's clean and idiomatic. It looks familiar to developers, doesn't involve any encoding/decoding or string mangling, and riffs on accepted semantics. Because DIDs always have at least two colons, confusion with hostnames (or IP addresses) is not actually ambiguous. If the URI syntax was valid, it felt like there might be a path to getting support elsewhere over time, like in browsers and generic URI parsing libraries.

What Can We Do About It?

There are billions of AT URIs in the wild today. Many of these are in content-addressed data records which can not be update without changing the record version (hash). Or in millions of cryptographically signed label objects from hundreds of providers. This means that the current syntax is going to be encountered and needs to be at least partially supported by atproto implementations indefinitely.

With that in mind, I think there are a few broad approaches the ecosystem could take:

Keep the existing syntax, and stop calling it a "URI": for example, call them "AT reference identifiers" or something like that. This would mean abandoning the IETF working group charter goal of standardizing the URI scheme. It would also cause endless confusion and ambiguity when trying to use the strings on the web, in other protocols, etc. I think this is a bad outcome and we should not take this path.

Keep the existing syntax, and get the IETF URI syntax rules changed: this would be a huge lift, sort of a "move the mountain" play. It would require a huge amount of standards body diplomacy and social capital, and would probably take years. I do think the outcome might be the best for both the atproto ecosystem and the internet protocol space generally. I think the URI/URL specification would be cleaner, and implementations clearer, if the authority part were more flexible in the generic case. This would make it easier for future protocols and URI schemes to use alternative semantics, without encoding or string mangling hacks. But it would be a big swing, and i'm not sure it would be wise to attempt it.

Change the AT URI syntax to be IETF compliant: there are a few different approaches we could take, which I discuss in a separate section below. None of them are as clean as the current syntax, and atproto implementations would need to support both versions, and handle normalization and equality checks carefully. Rolling out the change would take a long time, soak up developer time and attention, and some degree of user-impacting breakage feels inevitable along the way (because not all deployed software will support the new syntax). On the other hand, folks could start doing integrations with other software and protocols sooner, and the IETF working group would have an easier time getting through the initial charter.

My current feeling is that we should explore the second and third options in parallel. Figure out how much work and disruption these would actually be, come up with firmer proposals, and talk to folks. The venue for making a final decision should be the IETF working group (aka, on the mailing list), though we should take input from the broader atmosphere developer ecosystem in to account.

Alternative Syntax

How could we change the AT URI syntax to be valid under the existing IETF URI rules?

Personally, i'm broadly against approaches that involve different URI schemes; string encoding/decoding; alternative identifiers; or identifier mangling. I'll run through a few examples of those first.

One of the more direct approaches would be to percent-encode the DID in the authority section. So instead of at://did:plc:abc123/..., use at://did%3Aplc%3Aabc123/.... One advantage of this is that future DID methods with reserved characters could be used, or even other non-DID identifiers. But we know from their use in web URLs that percent encoding can be a total pain. They are ugly and unclear to look at, so folks will frequently display them in original form. Putting entire AT URIs in query parameters would involve double-encoding. There is no clear an reliable way to know if an input string has been sufficiently encoded/decoded. It is very easy to forget to encode when constructing a string. Decoding requires an extra string pass/copy and often an allocation. Blech. But I would guess this is the direction that standards folks will nudge us in.

A similar category of approaches would be to map DID syntax to allowed URI "host" syntax. For example, replace the colons with dashes (at://did-plc-abc123/...), or invent new hostnames (at://abc123.plc.did/...). These are cleaner-looking and avoid some of the encoding problems, but they do add a level of translation and indirection which is likely to cause confusion.

Some folks have proposed skipping the AT URI schema entirely and using "DID URLs", like did:plc:vwzwgnygau7ed7b7wt5ux7y2/at/app.bsky.feed.post/3k5nobkf2w72g. I don't like this because it is ambiguous how a DID URL string in the wild is supposed to be interpreted. You'd need to guess that it is "for atproto". It makes the URI strings less identifiable ("what is this for"). It would also lock the protocol in to use of DIDs long-run, and we might want to support non-DID persistent account identifiers in the future.

Other folks have proposed always using handles instead of DIDs in AT URIs (at://handle.example.com/...). This is already allowed under the AT URI specification, and follows the IETF URI rules. But this goes against the "persistent identifier" strength of DIDs in atproto. Handles can change, but DIDs stay the same for the entire lifetime of an account.

We could stop using // in the URI, either going with a single slash (at:/did:plc:abc123/...) or none (at:did:plc:abc123/...). I think folks would frequently type or correct the single slash version to at://. And the no-slash version looks more like a URN and could be confusing to new developers or end users. But I think these is moving in the right direction.

My current pick for an alternative syntax would be to go with an empty authority section and three slashes: at:///did:plc:abc123/.... This is allowed under the URI rules, and frequently shows up with file URIs (file:///home/root/notes.txt) where the authority part is optional. The DID ends up under the first path segment, and does not need to be encoded or mangled. Regular URI/URL libraries should work fine. It would be simple to disambiguate these from current AT URIs (just check if the strings starts at:///). It's just a single character change. On the other hand, this feels like it is bending the spirit of the URI syntax: we would be shifting the "authority" part to a "path" part just to resolve syntax restrictions. I'm not sure what IETF folks and the IANA URI registry reviewers would think about this. It looks a bit off visually, and folks are likely to "correct" these to double-slash manually.

What Else?

This post has focused on the primary use-case of AT URIs: globally referencing public records in the network. But there are a couple other things going on with atproto URIs which are worth noting.

A short form of AT URIs is used in DID documents to confirm DID/handle mapping. These show up in the "alsoKnownAs" array of URIs, and look like at://username.example.com. Handles are always valid hostnames, which means these are already valid URIs (and URLs). Changing all of these would require broad DID document updates, which would be a pain. I think we can simply keep this syntax for this use-case without too much confusion or disruption.

Design work is under way on "permissioned data" for atproto. This isn't under the current IETF working group charter, but we expect it to end up there under a future re-charter. Part of the current "spaces" proposal would include a new ats:// URI scheme ("Authenticated Transfer Space"). This scheme would have similar syntax to at://, including a "space authority DID" in the URI authority place. Whatever solution we come up with for AT URIs would presumably transfer over to ATS URIs.

Part of the IETF working group charter is coming up with criteria and a registry for "account identifier systems". The current atproto specifications require the use of DIDs, and specifically two DID methods are supported (DID PLC and DID Web). It is possible that non-DID identifier systems might end up being allowed by the working group. In general, the generic syntax of future account identifiers will be relevant to the AT URI syntax, and should be included as part of the final criteria.

There are some other small changes we could make to AT URIs, like blob references or record versioning. Those are relatively simple changes to the path structure, and don't touch on the core question here of authority part syntax.


That's the big picture! I expect this will be discussed over the next few months, both on the IETF ATP mailing list and elsewhere in the atproto developer ecosystem. It would be great if we could come to a rough consensus by the end of the year, perhaps at the IETF 127 meeting in November.