Atproto Web Names & Origins

Revision 1 - Initial ramble with the robots

Unofficial Draft

More details about this document
Latest published version:
https://atproto-web-names.szzt.app/
Editor:
@burrito.space

Abstract

Atproto web names is a proposal for resolving a short, human-readable name of the form @handle/name into loaded, attributable web content, using AT Protocol identity as the namespace root. It covers resolution of names to records, the vocabulary those records use to describe where content lives, the origin model that isolates loaded content, and the obligations on clients and publishers that make the result trustworthy.

It does not specify how any particular host stores bytes. Hosts are pluggable, and several existing ones are addressable without modification.

Status of This Document

This document is a draft of a potential specification. It has no official standing of any kind and does not represent the support or consensus of any standards organization.

This document refers to the general proposal as Atproto web names. The atw: link-scheme name is provisional; names marked TBD are placeholders.

This is revision 1, a working draft. It has not been implemented, and several rules rest on AT Protocol implementation behaviour that has not been verified against running servers; those are marked [verify] in Section 9. Section 15 lists what remains open. Numeric limits in Section 8 are first estimates and have not been validated against real content.

Interested parties should expect normative requirements to change.

Conformance

As well as sections marked as non-normative, all authoring guidelines, diagrams, examples, and notes in this specification are non-normative. Everything else in this specification is normative.

The key words MAY, MUST, MUST NOT, OPTIONAL, SHOULD, and SHOULD NOT in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.

Conformance

This document places requirements on two classes of implementation:

Requirements on handlers (Section 4.2.1) constrain what may be registered in the handler table rather than on any single implementation.

Examples and notes in this document are non-normative. Appendices A through C are non-normative.

1. Scope and design goals

This specifies how a client turns a short, human-readable name into loaded, attributable content, using atproto identity as the namespace root. Names arrive by any route a URL does: typed, clicked, pasted, shared, or embedded in a document.

An address looks like:

The identity segment is a namespace. The second segment is a short name within it. There is no per-site DNS name and no allocation registry.

In scope: resolution, the pointer record, the origin model, the client obligations that make the result trustworthy.

Out of scope: how any particular target hosts its bytes. Atproto web names can point at wisp, standard.site, IPFS, plain HTTPS, or any other system with a handler. Supporting a host requires a handler, not changes to the host's storage or publishing mechanisms.

This draft covers public websites (Section 16). Access-controlled content already has a defined handling; cryptographically private content is deferred to atproto's own private-data work and enters at a single identified extension point (Section 4.3).

1.1 Design goals

In priority order. Where they conflict, the higher one wins, and several rules elsewhere in this document are that conflict being resolved.

1. A name never claims more than it can substantiate.

This is the governing rule, and it decides more of the spec than any other. The name is what this design exists to provide; if @portable.agency/blog can mean "proven to that identity" or "some host asserted this" with nothing distinguishing them, the design has no value over a plain link.

Everything downstream follows: dispositions rather than a grading scale (Section 4.3), no fallback to a weaker target (Section 4.4), no derived origin for unverified content (Section 5.1), no stale handle shown anywhere a name appears (Section 5.10), no link rendered as resolved before it is (Section 5.9).

Its converse sets the tolerance for everything else (Section 13.3): imperfections in reach and recovery are accepted; imperfections in what a name claims are not.

2. Identity is the namespace root, and it must survive its keys.

Names derive from a DID, never from a key (Section 1.2), so rotation, recovery, and handle changes leave names and storage intact. A design where a key mistake is terminal is not one an individual can run for years.

3. Verification belongs in the client.

Every intermediary is then a cache that cannot lie, so hosting becomes a performance decision rather than a trust decision (Section 7). This is what makes mirrors free, gateways optional, and self-hosting meaningful.

4. Portable across platforms, not tied to one.

Resolution and description (Layers 1–2) are identical everywhere. Only loading varies, and Section 5 specifies properties rather than mechanisms, so an Electron client, a mobile client, and a compatibility gateway can all be conforming.

5. Layers compose, and none depends on the one above it.

Resolution works without loading; loading works without petnames; petnames work without any handler (Section 5.11.5). The naming layers (Section 5.10) are the clearest case: a DID resolves with no handle, a handle works with no petname, and a petname survives a handle that stops resolving.

This is why optional layers can be left to clients without leaving the design incomplete, and why a later addition does not become a flag day.

6. Extensible without a flag day.

New hosts are new handlers, not spec revisions (Section 4.2). New fields are ignorable unless the publisher marks them critical (Section 4.1). A design that requires coordinated upgrades will not get them.

7. Good enough, not perfect.

This design need not solve every gap already present in the HTTP web. Where it shares such a gap, that may be a known cost rather than a blocker; Section 13.3 lists the ones accepted deliberately. This is a limit on what the design must solve, not a goal of matching HTTP or the web platform. HTTP is usable as a transport, not a feature-parity target.

1.2 Non-goals

1.3 Design posture

The substrate is heterogeneous, non-hostile, and imperfect. Many PDS implementations exist and more will. They are not adversaries, but they vary, they have warts, and parts of their behaviour are unspecified by atproto itself.

Three consequences shape the design:

Verification handles lying; it does not handle varying. A PDS cannot forge a signature or substitute content-addressed bytes, so dishonesty is already answered. The exposure is everything verification does not touch: liveness, rate limits, reclamation timing, optional endpoint support, and error semantics.

Prefer mitigations that are safe everywhere over mitigations that are necessary somewhere. Where implementation behaviour is unspecified, the publisher-side answer should be harmless on implementations that did not need it. Section 9.3's staging records are the model: unnecessary overhead against a PDS with lazy blob reclamation, essential against an eager one, and correct without knowing which you have.

Goal 7 (Section 1.1) applies to the substrate too. PDS implementations will have bugs, gaps, and divergent behaviour, exactly as HTTP servers do. The design should degrade rather than break, and should not wait for a uniform substrate that will never arrive.

1.4 Layering

Layer What it does Portable?
1. Resolution (identity, name) → route record Yes, identical everywhere
2. Description What the record says the name points at Yes
3. Loading Fetching and rendering a target No; platform-specific

Layers 1 and 2 are normative for every implementation. Layer 3 specifies required properties, not mechanisms.

1.5 Lifecycle

Every reject below is a hard failure rendered as client chrome (Section 5.7); no branch degrades to a weaker outcome (Section 4.4).

Resolution and loading lifecycle: verify identity and records; reject invalid proofs; redirect asserted targets; load verified bytes into a derived origin.
Figure 1 Resolution and loading lifecycle. Open the full-size diagram.

2. Terminology

2.1 Naming

Identity. An atproto account, named by a DID, usually addressed by its handle.

Short name. The second address segment. Unique within one identity, enforced by record key uniqueness in the repo.

Root name. An address with no short name, resolving to the reserved record key self (Section 4.7).

Petname. A label a user assigns to an identity, local to that user's client and bound to a derived origin (Section 5.11). Distinct from a handle, which the publisher assigns, and from a short name, which is a path segment.

Display form / link form / record form. The three forms a name takes (Section 3.1). Display form is @handle/name; link form is atw://handle/name; record form is the AT-URI of the route record.

2.2 Records

Route record. The record a short name resolves to. Collection agency.portable.route (TBD), record key = the short name.

Target. One entry in a route record: a URI plus optional transport hints.

Alias. A route record naming another short name in the same identity instead of carrying targets (Section 4.7.1).

Switching record. The single record whose write is the moment a site changes. Every handler must resolve through one (Section 4.2.1).

Generation pointer. A switching record for a handler whose content accumulates across many records, naming which set is current.

Staging record. A record written during publishing solely to keep uploaded blobs referenced. No handler resolves them (Section 9.3).

2.3 Proof and disposition

Provable. A target whose bytes are proven to the identity that named them, by either proof mechanism below.

Asserted. A target whose location the identity states but whose content nothing proves.

Proof mechanism. How a provable target is proven. The set is closed: inclusion, where content lives in a record proven by MST inclusion under a signed commit; and content-address, where a proven record names a CID against which served bytes are re-hashed (Section 4.3.1).

Disposition. What a client does with a target: load, redirect, or reject (Section 4.3).

2.4 Loading

Handler. Client code that fetches and renders one class of target, keyed by collection NSID for at:// targets and by scheme otherwise. The web does the same dispatch twice, on Content-Type and on URI scheme, and calls the code a handler in both cases; this is that, keyed on a third thing. A client supporting a given handler is the analogue of a browser supporting a given media type: a matter of reach, not of conformance (Section 10.2).

Transport. Where bytes are fetched from, as distinct from which content is fetched. Mirrors and caches are transports; choosing among them is not a security decision (Section 4.4).

Mirror. An advisory alternative transport for content-addressed bytes, listed as a hint on a target.

Derived origin. The origin a provable target loads into, computed from the identity and short name (Section 5.1).

Floor. The highest (rev, contentCID) pair a client has seen for an origin, below which content is refused (Section 5.2). Client metadata, not site storage.

Content CID. The CID of the selected target's switching record (Section 4.2.1). Every handler resolves through exactly one, so this identifier always exists and changes exactly when the site changes.

Capability set. Which proof mechanisms, handlers, and schemes a client implements (Section 10.2).

Intermediary. A cache or gateway between a client and a PDS. An intermediary serving com.atproto.sync.* performs no selection; one serving a compatibility HTTP surface does (Section 10.3).

3. Layer 1: Resolution

3.1 Address forms

A name has three forms. They serve different purposes and are not interchangeable.

3.1.1 Display form

How a name appears wherever a person reads one:

3.1.3 Record form

The AT-URI of the route record itself:

This identifies the record and is what Section 3.3 resolves. It is not a link form and SHOULD NOT be published as one, for two reasons: AT-URI syntax terminates at the record key, so it cannot carry the subpath that Section 3.3 requires be passed to the handler; and it embeds the route collection's NSID, which is unsettled (Section 15), so every published link would depend on a vocabulary decision that has not been made.

at:// also already means "identify a record" across the wider ecosystem, including in clients that resolve it to record viewers. A link asks for something to be rendered, which is a different request.

The two schemes are neighbours by design. at:// identifies a record; atw:// renders a site. Sharing a prefix makes them read as one family, the way http: and https: do, and the shared shape is accurate: both name an identity as the authority over what follows. They differ in what they ask for, not in who governs it.

3.1.4 Handling

Clients MUST accept display form and link form as names to resolve, and MUST treat them as equivalent. @b.example/notes and atw://b.example/notes name the same thing.

Clients SHOULD display display form, and MUST be able to display it for any origin they have created. A root name SHOULD display as @<handle> with no trailing segment.

An at:// URI naming any collection other than the route collection is outside this specification. Clients MUST NOT treat such a URI as a name to load, and a client that handles it otherwise is doing something this document does not describe.

Segments after the short name are the subpath, passed to the handler (Section 4.2.2). They take no part in resolving the route record, whose record key is the short name alone.

A query component is passed to the handler with the subpath. It takes no part in route record resolution, in path canonicalization (Section 5.6.1), or in origin derivation (Section 5.1). A handler MAY ignore it.

A fragment is client-side only. It MUST NOT be transmitted, and takes no part in resolution, canonicalization, derivation, or the floor.

A trailing separator with no short name, as in atw://b.example/, resolves to the root name (Section 4.7). It is not a name of empty string, and Section 6.2's listing is a client affordance rather than a different resolution.

3.2 Identity resolution

  1. Resolve handle → DID by both methods below.
  2. Resolve DID → DID document.
  3. Verify the binding is mutual. The DID document's alsoKnownAs MUST contain at://<handle>. A handle that resolves to a DID which does not claim it back MUST be rejected.

Step 3 is not optional. Without it a hostile resolver returns any DID it likes and the handle means nothing.

Clients SHOULD perform DNS resolution themselves over DoH rather than delegating to a third-party resolveHandle endpoint. A client that does delegate MUST still perform step 3, which reduces a hostile resolver to denial of service rather than impersonation.

DID documents MAY additionally be resolved over alternative transports (Section 14.3). These are redundancy, never a requirement.

3.2.1 Required bindings

This specification is a profile of AT Protocol, and the following are normative rather than left to the implementer. Field semantics come from the AT Protocol identity, repository, data model and cryptography specifications, which an implementer needs alongside this one.

Handle resolution. Clients MUST attempt both the _atproto.<handle> DNS TXT record and https://<handle>/.well-known/atproto-did. Where both resolve and disagree, DNS wins. Where only one resolves, that result is used. Handles are compared and stored lowercase, after IDNA processing (Section 13.2).

DID methods. Clients MUST support did:plc and did:web. A client supporting neither cannot resolve anything; a client supporting one is not interoperable, since publishers choose independently.

The did:plc directory endpoint is configuration, not a constant. It is a trust anchor and a single point of failure, so clients MUST allow it to be changed and SHOULD allow a mirror to be used for redundancy (Section 14.3).

DID document fields. The repo signing key is the verification method whose id fragment is #atproto. The account's host is the service entry whose id fragment is #atproto_pds. Clients MUST select by fragment and MUST NOT assume array position. Where either is absent, resolution fails.

Signature curves are those the AT Protocol cryptography specification blesses. A client MUST support all of them, since a publisher's key type is not the reader's choice.

The route collection NSID is configuration. Until Section 15 settles it, implementations MUST read it from configuration rather than compiling it in, so that fixing the name is a settings change rather than a release.

TTL bounds. A client MUST clamp a cached handle resolution to at least 60 seconds and at most 24 hours regardless of the TTL served. Honouring a zero TTL makes every display a DNS query (Section 5.10.2); honouring a month-long one makes a transfer invisible for a month.

3.2.2 Account states

A DID resolving does not mean its content should load.

State Behaviour
Active Load normally
Migrating Load normally; the DID is stable across PDS moves; follow the current serviceEndpoint
Deactivated Do not load. Client chrome, per Section 5.7
Suspended Do not load
Taken down Do not load, cache-wide, for every origin under the DID

Takedown is the state moderation uses in practice, and the only one that reaches back into already-cached content. Clients SHOULD treat a PDS reporting takedown as authoritative for that repo even when content-addressed bytes remain available from a mirror, the mirror cannot lie about the bytes, but it also cannot know the account's state.

Takedown stops loading. It MUST NOT erase user data. Clients MUST NOT delete storage in a derived origin in response to any account-state signal.

Account state is reported by the PDS, the party this design declines to trust. Erasing storage on takedown would let a hostile operator destroy local data for every user who ever loaded that publisher's sites, a stronger capability than any attack this spec defends against. Withholding a load is recoverable; erasure is not, and is the user's action to take.

Note the asymmetry this creates with Appendix B: a mirror serving proven blobs will continue to satisfy requests from clients that never re-resolve. Takedown is a request to stop, not a guarantee of erasure (Section 13).

3.3 Name resolution

The short name is the record key.

Consequences of using the rkey:

Clients MUST use com.atproto.sync.getRecord, not com.atproto.repo.getRecord. Only the sync endpoint returns the MST blocks proving the record's inclusion in the signed commit. The repo endpoint returns the record on the host's say-so. An implementation that uses the repo endpoint appears to work identically and has silently removed the entire verification story.

Clients MUST verify:

Short name syntax is atproto record key syntax: 1–512 characters from A-Za-z0-9.-_~:, and not . or ... Record keys are case-sensitive; so is this address segment. Clients SHOULD warn on case-only collisions during publishing.

Subpaths. Address segments after the short name are passed to the handler, which interprets them. A route record names a site; the handler decides what /welcome means inside it.

3.4 Caching

Two layers with different invalidation rules. Conflating them is the most common way to get this wrong.

Pointers (handle→DID, DID→doc, DID+name→route record→root CID) are mutable.

Content (CID→bytes) is immutable and never invalidates. Shared across versions, names, and identities. This is where hit rate lives and it belongs on disk.

Clients with a persistent process MAY subscribe to a filtered firehose for push invalidation, the model wisp hosting services already use (Section 14.1). Filtering by collection rather than by repo means this does not disclose which names the user visits.

The firehose is an invalidation signal, not a source of proven content. Simplified streams such as jetstream emit JSON without the CAR blocks that carry the signed commit, so records taken from them are unproven. A cache built by writing firehose records straight into a store and serving them has no verification story at all, however faithful the bytes.

This matters because it is the cheap path and the standard pattern. Implementations MUST re-fetch via com.atproto.sync.getRecord to obtain proof, using the stream only to learn that something changed. A (collection, rkey) pair from the stream tells a cache exactly which entry to invalidate; the proof comes from the sync endpoint.

rev is a per-repo cache epoch. com.atproto.sync.getLatestCommit returns {cid, rev} without a CAR, so one cheap request revalidates every name under an identity at once. This is a stronger primitive than per-resource validators, closer to a surrogate key than an ETag, and intermediaries should build on it. Note it advances on any repo write, including ones unrelated to any site (Section 5.2), so a cache SHOULD compare the commit's touched collections before invalidating site entries.

Snapshot consistency. Where a handler assembles a site from multiple records, clients SHOULD pin one page load to a single commit rev and resolve every resource against it, and publishers SHOULD write all records for one logical update in a single commit via com.atproto.repo.applyWrites. Without both, a reader can observe a torn state, new index, old article. See Appendix B.

Offline and stale pointers. Content addressing makes offline operation work well, cached bytes verify against a cached CID with no network at all. The mutable half needs a rule.

4. Layer 2: The route record

4.1 Lexicon

Exactly one of targets or alias MUST be present, unless status is retired, in which case both MUST be absent. A record carrying both, or neither without being retired, is invalid and MUST NOT load.

4.1.1 Retirement

A missing record and a retired record mean different things and MUST be handled differently.

Record absent status: retired
Means Unknown; may be transient Deliberately withdrawn
Client behaviour Fail, treat as possibly temporary Fail, state it is permanent
Origin storage Retain Retain; MAY offer the user a clear action
successor n/a MAY be offered as a link, never auto-followed

A client cannot distinguish "deleted" from "the PDS is down" without this, so deletion alone strands an origin's storage with no explanation anyone can give the user.

Auto-following a successor is forbidden for the same reason Section 4.4 forbids fallback: a retired name's successor is chosen by whoever controls the DID now, which may not be who the user trusted.

A target is a URI plus optional transport hints. Hints MUST NOT be required for correctness: a client ignoring every hint MUST still be able to load the target by resolving its URI.

mirrors is the one defined hint. It lists alternative sources for the same content, tried in any order alongside the canonical source (Section 4.4):

A mirror serves the same content-addressed bytes and is checked against the same CIDs, so a hostile mirror can withhold but cannot substitute. Mirrors MUST NOT be used to reach content whose CID is not already known from a proven record.

4.1.2 Unknown fields

Handling is asymmetric by design.

  • Unknown properties on a target or on the record MAY be ignored. They are additive; ignoring one loses a hint, not a guarantee.
  • Unknown properties inside policy are governed by the critical list. A client encountering an unrecognized policy key named in critical MUST fail closed. One not named there MAY be ignored.

A blanket fail-closed rule on unknown policy keys would make the vocabulary permanently unextensible: adding any field would hard-fail every older client, a flag day for every future extension. A blanket ignore rule inverts Section 5.3 silently, a publisher tightens something, an older client drops what it cannot parse, and the content loads with looser capabilities than the publisher declared.

The critical list lets the publisher decide which it is, per field:

Publishers MUST list any restriction whose omission would change behaviour unsafely, and SHOULD NOT list fields that merely hint.

A client that does not implement critical MUST fail closed on any unrecognized key inside policy. Such a client cannot determine which unknowns are critical, so it must treat all of them as if they were. Without this rule the mechanism is an open downgrade: an older client would ignore both critical and the restriction it names, and load with looser capabilities than the publisher declared.

This is why critical is defined in the first revision rather than added later. A client predating it has no safe reading of a record that uses it, and the rule above is what makes that predictable rather than silent.

It also constrains how this specification may change. A later revision that forbids something previously allowed cannot be made safe by any declaration an older client makes, since that client cannot refuse a rule it has never seen. Such a change MUST therefore arrive either as a policy field publishers mark critical, or as a new origin derivation byte (Section 5.1) so the content loads into an origin older clients never enter. A restrictive change with neither door is forbidden regardless of its merit.

This is the general rule for any future extension. Fields that grant or hint are ignorable; fields that restrict are ignorable only if the publisher says so.

4.1.3 Example

NSIDs beginning example. below are placeholders standing for whatever handlers a publisher's content actually uses.

4.2 Handler dispatch

For at:// targets, the collection NSID is the handler key. Adding support for a host is registering an NSID in a table.

The table below is illustrative, not normative. Its entries are existing or prototype systems, included to show the range of shapes a handler can take; none is privileged by this specification, and a conforming client may implement any subset or none of them (Section 10.2).

NSID Shape Proof
place.wisp.fs manifest record → compressed blobs content-address
site.standard.publication publication → documents, matched by path inclusion
pub.leaflet.document document renderer inclusion
app.szzt.site site record → deployment → manifest → blobs content-address

Non-at:// targets dispatch on scheme.

No spec revision is needed to support a new host, there is no target type union to extend, and the record stays readable by anything that already parses AT-URIs. Which handlers exist is an ecosystem question rather than a specification one; what this document constrains is the shape a handler must have (Section 4.2.1) and how a client declares which it implements (Section 10.2).

There is no registry. Handlers are self-describing by NSID, clients implement whichever they choose, and a client encountering an unimplemented handler skips that target and says so (Section 10.2). If enough handlers exist to make coordination worthwhile, a registry is something the ecosystem can add without this document changing.

4.2.1 Handlers MUST have an atomic switch

Every handler MUST resolve its target through exactly one record whose write is the moment the site changes. wisp's manifest record is one example; a handler that assembles a site from N records with no single switch cannot publish atomically and MUST NOT be registered.

Publishers cannot fix this themselves: atproto gives atomicity within one commit and large publishes do not fit in one (Section 9.4), so the thing that flips must always be one small record.

For inclusion-proven handlers this means a generation pointer: content records accumulate under any rev, and a pointer record names which generation is current. Readers resolve through the pointer, so partial writes are invisible until it moves.

4.2.2 Handler contract

A handler is code, and this specification does not define one. A client that loads anything therefore needs at least one handler specification, which is separate work. What this section fixes is the boundary between them, so that a handler written against one client works in another.

A client gives a handler:

Target URI The entry selected in Section 4.4
Subpath Address segments after the short name, canonicalized per Section 5.6.1
Query Passed through; the handler MAY ignore it (Section 3.1.4)
Pinned revision The commit the load is pinned to (Section 3.4)
Byte range Optional, for media (Section 11)

A handler returns, for a request it can satisfy:

Bytes The resource
Declared content type Never sniffed by the client (Section 5.5)
Switching record CID The content CID for the floor (Section 5.2)
Proof mechanism Inclusion or content-address (Section 4.3.1)

A handler returns a path-level miss for a path absent from a site it has otherwise proven, which is after-proof content under Section 5.7. Anything else is a failure, and failures are the client's to render.

A handler specification MUST state its path resolution rules (Section 5.6), its switching record, and its resource size ceiling (Section 8). A handler MUST NOT fetch anything the client did not authorize, and MUST NOT return bytes it has not verified.

4.3 Dispositions and proof

A target is provable or asserted. This determines the disposition, and the disposition determines everything downstream.

4.3.1 Provable → load

The bytes are proven to the identity that named them. Two mechanisms:

Proven by inclusion. Content lives in the record, proven by MST inclusion under the signed commit (Section 3.3). No hashing step; the proof from resolution is the proof of the content. site.standard.document and pub.leaflet.document work this way.

Proven by content address. The record names a CID; every byte served is re-hashed against it; a mismatch is a hard failure. place.wisp.fs works this way. An ipfs:// target qualifies because its CID came from the proven route record, not because IPFS is involved.

Provable targets load into a derived origin (Section 5.1).

4.3.2 Asserted → redirect

The identity asserts a location. TLS authenticates the host. Nothing authenticates the content. https:// targets, and the url field of a site.standard.publication.

An asserted target is a signpost, not a backend. The client navigates away to the target's own origin under ordinary web rules. It MUST NOT synthesize an origin, and wherever it displays the current location it MUST show the destination, not @handle/name.

Displaying the name over an unverified third-party origin is worse than a plain link: it lends the identity's authority to a host the identity only asserted. The name got the user there; it does not vouch for what is there.

4.3.3 Neither → reject

http://, or any case where the chain breaks. Clients MUST fail rather than degrade.

4.3.4 One handler per target

A target has exactly one handler and one disposition. A site.standard.publication target loads from its document records; its url field is a separate target the publisher may also list, with its own disposition. Same site, two entries, two behaviours, not one entry interpreted two ways.

4.3.5 Disposition is viewer-independent

A target's disposition MUST NOT depend on who is asking. It is fixed by the record and the client's handler support, never by authentication state, session, or identity of the viewer.

An implementation that gives an authenticated viewer a load and an anonymous one a redirect makes the origin vary per viewer: storage materializes and vanishes with auth state, and Section 5.2's per-origin rollback floor becomes meaningless. The tempting case is auth-gated HTTPS content, which is asserted for everyone (Section 14.1).

This invariant is what lets new proof mechanisms be added later without retroactive storage loss. Breaking it is cheap now and expensive to retrofit.

4.3.6 Extension point

The two mechanisms above, inclusion and content-address, both prove against a public signed commit. That is the only reason Section 16 scopes this draft to public content.

A future mechanism proving content that is not publicly readable slots in here and nowhere else. Resolution (Section 3), origins (Section 5.1), rollback (Section 5.2), and capability policy (Section 5.3) are all orthogonal to whether the bytes are readable, and none of them should need revision.

4.4 Selection

Clients MUST select the first target whose handler and disposition they support, and then stop.

If the selected target fails, that is a hard failure. A client MUST NOT fall through to a later target on error.

Falling through on error means anyone who can break the first fetch forces the client onto a weaker target, and the weakest entry in the list silently sets the security level of the name. Ordering expresses publisher preference among targets the client can handle. It is not an error-recovery mechanism.

This constrains targets, not transports. Retrying the same content from a different source, a second mirror, a cache, the origin PDS after a cache miss, is not a fallback and is explicitly permitted. Content addressing makes the choice of transport free: every source is checked against the same CID, so none of them can be the weak one.

The distinction has to be drawn or a careful implementer reads Section 4.4 as forbidding mirror retry, which removes the resilience Section 7 depends on. A target says what content; a transport says where to get it. Only the first is a security decision.

Clients MUST select the first provable target they support, and MAY select an asserted target only if no provable target is supported.

Stated as "prefer the provable one where both describe the same content" this would be unimplementable: a client cannot know that https://blog.example.com/ and at://…/example.handler.site/blog carry the same bytes without loading both, which the rule above forbids. Any implementation of that phrasing would be a heuristic, and a heuristic here is attack surface. Ordering among provable targets is publisher preference; the provable/asserted boundary is not negotiable by ordering.

4.5 Redirect budget

A redirect may land on a URL that resolves back through a route record. Loops are ordinary control flow rather than an edge case, and they can span record types: route → publication → url → route.

Clients MUST cap redirect hops at the limit in Section 8 and MUST treat exhaustion as a hard failure rather than selecting another target (Section 4.4). Loop detection MUST span record types.

4.6 What a name claims

A route record asserts only: the controller of this DID says this short name points here.

That is strong for provable targets and weak for asserted ones.

For asserted targets the binding MAY be upgraded to mutual with a reverse proof from the target, a TXT record under the target domain carrying the route record's AT-URI, or standard.site's <link rel> pattern (Section 14.4). Clients that find one SHOULD indicate it.

This is optional by design. Requiring it kills the "just point at my existing blog" case, which is the case that gets people using this.

4.7 Root name

An address with no short name resolves to the reserved rkey self, following the atproto convention for singleton records.

self is an ordinary route record in every respect: same targets, same dispositions, same proof, same origin derivation. Nothing about Section 4.3 or Section 5 special-cases it.

@<handle> and @<handle>/self are two spellings of one name. Clients MUST resolve them identically and MUST derive the same origin for both. An implementation that derives from the empty string for one and "self" for the other produces two origins for one site, with storage and rollback state split between them.

self is therefore not available as an ordinary short name, a publisher writing to that rkey is configuring their root.

Absence is not an error to paper over. If no self record exists, @<handle> does not resolve. Clients MAY offer a directory of the identity's names via com.atproto.repo.listRecords (Section 3.3), but such a directory is client chrome, not content: it MUST NOT load into a derived origin and MUST NOT be presented as the identity's site.

4.7.1 Aliases

A route record MAY alias another name in the same identity instead of carrying targets:

This exists because "my root is my blog" is the common case, and the alternative, duplicating targets into self, silently creates a second origin for what the publisher believes is one site.

  • An alias MUST resolve before origin derivation. @portable.agency aliasing blog loads into blog's origin, not a separate one. Same site, same storage, same rollback floor.
  • Aliases MUST NOT chain. An alias target MUST be a route record carrying targets. An alias pointing at another alias is a hard failure.
  • Aliases are scoped to one identity. Cross-identity aliasing would let one identity's name adopt another's origin, which Section 5.1 exists to prevent. Point at the other identity with an ordinary target instead.
  • A record MUST NOT carry both alias and targets.
  • An alias naming a record that does not exist is a hard failure, not an empty site. A dangling alias means the publisher's intent is unknown, and Section 5.7 puts unknown intent in client chrome.

The no-chaining rule removes alias loops by construction rather than by budget, which is why Section 4.5's hop cap does not apply here.

5. Layer 3: Loading

5.1 Origin model

Requirement, not mechanism: each (identity, short-name) pair gets a distinct origin. Names under one identity do not share storage with each other. Loaded content never shares an origin with privileged client surfaces.

The origin string will differ per platform. The property must not.

Derivation:

Encoding is RFC 4648 base32, lowercase, without padding. The version byte for this revision is 0x01. did and short_name are their UTF-8 bytes, with the DID in the form it appears in the DID document.

The label is the full 53 characters and MUST NOT be truncated. A 33 byte input encodes to 53 base32 characters, which fits the 63 character limit a DNS label allows and therefore every platform host slot in Section 11. Truncating would weaken collision resistance and let two platforms derive different origins for one name.

Only load targets get a derived origin. redirect targets use their own origin (Section 4.3). The client MUST NOT synthesize a durable origin for content it cannot verify, otherwise anyone who compromises that host inherits everything accumulated under the name.

This asymmetry decides the rule. With content-addressed origins, substituted bytes produce a different origin and land in an empty sandbox, failing safe. With name-addressed origins, substituted bytes land in the same origin with full access to accumulated storage. Name unification is only sound where resolution-to-bytes is proven end to end.

5.2 Anti-rollback

Clients MUST record, per origin, the highest (rev, contentCID) pair seen, and MUST refuse content proven against an older commit when the content CID differs.

A proof whose content CID equals the highest known content CID is accepted regardless of its rev. The rev comparison applies only when the content itself differs.

The content CID is the CID of the selected target's switching record (Section 4.2.1). Using the switching record rather than a root manifest CID makes the rule uniform: Section 4.2.1 requires every handler to have exactly one such record, and its write is by definition the moment the site changes, so its CID is both always available and exactly the right identity to compare.

rev values are TIDs and MUST be compared as ASCII strings. TID encoding is sortable, so string comparison and chronological comparison agree. Clients do not need to consult the device clock (Appendix A.2).

rev is a TID the PDS enforces as monotonically increasing per repo, so unlike a publisher-declared version number it is not forgeable by the publisher. This is the atproto-native answer to the problem IWA solves with update-manifest version numbers (Section 14.2).

It closes the multi-transport skew hole: a stale mirror or unupdated IPFS pin serving an older deployment into an origin that newer code has already written storage into.

5.2.1 Content identity in the floor

rev advances on any write to the repo, and a site shares its repo with its publisher's posts and follows. An active poster churns their epoch continuously while their site is unchanged, so a mirror holding a valid proof of identical content at an older rev would be refused under a rev-only rule. The rejection is spurious, it fires more often the more active the publisher, and the obvious workaround, always fetch the origin PDS for a current rev, deletes Section 7 entirely.

The pair gets all three cases right:

Situation Content CID Rev Outcome
Attacker serves superseded content Differs Lower Refused
Mirror serves current content, stale proof Same Lower Accepted
Publisher deliberately reverts Differs Higher Accepted

The floor is about content, not commit position. rev is how content changes are ordered; it is not itself the thing being ordered.

5.2.2 Floors are client metadata

The rev floor is durable per-origin state, and Section 5.4 forbids drive-by navigation from creating durable state. These do not conflict, but the resolution must be explicit or implementations will pick one and break the other.

A floor is client metadata, not site storage. It is written by the client, unreadable by the site, and recorded for every origin including ephemeral ones, skipping them would disable rollback protection for exactly the unvetted sites most likely to be hostile. The table must be bounded:

  • Clients MUST bound the floor table and SHOULD evict least-recently-used entries for ephemeral origins first.
  • Clients MUST NOT evict a floor for a pinned origin while its storage persists. Evicting a floor silently lowers it, so a floor must never outlive the data it protects, nor the reverse.
  • Eviction is not a downgrade signal. A re-encountered origin with no floor starts fresh, which is where a first visit already stands.

5.2.3 Rotation resets the floor

A change to the DID document's verification method MUST reset the rev floor for every origin under that DID, and SHOULD discard content cached against the superseded key.

Without this, the floor blocks the recovery it is supposed to survive. Run the compromise case: an attacker obtains a signing key, publishes a malicious deploy, a client loads it. The origin now carries a high floor and attacker-written storage. The publisher rotates, but the obvious recovery, republishing known-good content, is refused by the very rule meant to protect them, and any client holding the malicious deploy keeps serving it until it re-resolves.

Rotation is the recovery signal, and it is the only one available: the old key can still sign anything, so no rule expressed in terms of rev alone can distinguish a rollback attack from a rollback repair.

The reset is itself an attack primitive, and must be guarded. Whoever controls the rotation keys can rotate and then serve arbitrarily old content, rolling a site back to a version with a known vulnerability, into an origin holding storage written by newer code. Rotation-key compromise therefore grants republish and rollback, where without this rule it would grant republish alone.

The guard is the recovery window. For did:plc, a rotation can be overridden by a higher-priority key within the operation log's recovery window, which exists for exactly this case.

  • Clients MUST NOT honour a floor reset until the rotation has settled past the recovery window.
  • Until it settles, the previous floor stands, and content signed by the new key that falls below it does not load.
  • Clients SHOULD record which verification method each floor was established under, so the reset fires on rotation and not on unrelated DID document edits.
  • For DID methods with no comparable window, clients SHOULD require user confirmation rather than applying a reset silently.

This is what makes Section 1.1's separation of identifier from key meaningful. A design where rotation does not reset the floor still loses the site on key compromise, the loss is merely deferred to the first recovery attempt. A design where it resets unguarded trades that for a rollback channel.

5.3 Capability policy

Content-addressed verification is worthless if a verified page loads a remote script. The bytes of the HTML are proven; what executes is not.

A publisher-supplied policy may only narrow the client's defaults, never widen them. The record is publisher-controlled input, not configuration.

5.3.1 Non-manifest script vectors

Manifest-only resolution governs <script src>. Three vectors never reach a manifest and MUST be handled explicitly:

  • javascript: URLs MUST be rejected everywhere, including as the resolved destination of a cross-name link (Section 5.9). This is the one reachable from another publisher's content.
  • data: URLs MUST NOT be a script source, and MUST NOT be navigable at top level. Inline as manifest entries instead; a data: script is unproven bytes executing in a proven origin.
  • blob: URLs are permitted only for blobs created by already-proven script within the same origin. They inherit the creating origin, so the constraint is that the creator was itself proven.

Inline <script> and eval are governed by the client's baseline CSP. Clients SHOULD forbid both by default, since a site assembled from a manifest has no need for either.

5.3.2 The Origin header

Clients MUST define what Origin a derived origin sends on outbound requests, and SHOULD send the derived origin's own scheme and host rather than omitting the header or sending null.

Section A.2's note that CORS is unnecessary concerns this design's fetches of content-addressed blobs. It says nothing about how existing services behave when called from an unfamiliar scheme. Servers with string-matched allowlists will encounter a scheme they have never seen; omitting the header or sending null is worse than sending a value, because null is a value some servers treat as same-origin.

5.4 Ephemeral and pinned origins

Drive-by navigation MUST NOT create durable state that the site can write or read.

Ephemeral (default) Pinned
Created by Navigation Explicit user action
Storage Session-only Persistent
Permissions None Promptable
Quota Shared, capped Own allocation

This section governs site-visible state. Client metadata about an origin, notably the rollback floor (Section 5.2.2), and a petname where one exists (Section 5.11), is written by the client, is not readable by the site, and is recorded for ephemeral origins as well. Omitting a floor for unpinned origins would disable rollback protection for exactly the unvetted sites most likely to be hostile.

Without this tier, unbounded origin creation becomes quota exhaustion and permission prompts from origins the user cannot evaluate, the problem that stalled IWA (Section 14.2).

Promotion is also where preference labels apply (Section 5.13.3). Safety labels are consulted earlier, before any load.

5.5 Content types

Type comes from the manifest or record. Clients MUST NOT sniff.

An entry with no declared type MUST be served as a non-executable default (application/octet-stream), never guessed from its bytes or extension.

Content addressing does not help here. A text file whose bytes sniff as HTML is exactly what the publisher signed, so a sniffing client executes script in the derived origin with a valid proof. This is the mistake that produced X-Content-Type-Options: nosniff; there is no reason to make it a second time when the manifest already enumerates every resource.

atproto blob references carry mimeType alongside the CID, so for blob-backed handlers the type is already proven by the same chain as the bytes. Handlers SHOULD use it rather than introducing a parallel declaration.

5.6 Path resolution

Each handler MUST define, and each client MUST follow:

Clients MUST NOT apply their own defaults here. Every HTTP server answers these differently, and the difference is observable because relative URLs inside a document resolve against the result. Two clients disagreeing means the same proven bytes render differently, which defeats the purpose of proving them.

5.6.1 Canonical path form

Before any lookup or escape check, a path MUST be normalized:

  1. Percent-decode once, and only once. A second decode pass is how %252e%252e becomes traversal.
  2. Apply Unicode NFC.
  3. Reject any path containing a NUL or a raw control character.
  4. Collapse repeated separators: a//b → a/b.
  5. Remove . segments.
  6. Resolve .. segments. A .. that would ascend past the root is a hard failure, not a clamp to root.
  7. Reject backslash as a separator rather than translating it.

The same normalizer MUST produce the path used for the Section 5.3 escape check and the path used for manifest lookup. Normalizing twice, or checking one form and looking up another, is the classic traversal bug: the check passes on a form the lookup never sees. Handlers MUST NOT re-normalize after the check.

Manifests SHOULD store paths already in canonical form, and clients SHOULD reject a manifest containing any entry that is not canonical rather than normalizing it on the publisher's behalf. A non-canonical entry is either a mistake or an attempt to present two spellings of one resource.

5.7 Error surfaces

Failures fall on two sides of proof and MUST be handled differently.

Before proof. The name does not resolve; no self record exists (Section 4.7); the record is retired (Section 4.1.1); the account is deactivated, suspended, or taken down (Section 3.2.2); an alias is dangling or chained (Section 4.7.1); no target is supported (Section 9.2); verification fails; the rollback floor is violated (Section 5.2); a limit is exceeded (Section 8). These MUST render as client chrome. Rendering publisher-supplied content for a verification failure means rendering unproven bytes into the origin, which Section 10 forbids outright.

After proof. A path that does not exist within a proven site. This is ordinary content. The handler decides; a handler's configured not-found path belongs here, and the response loads into the origin like any other proven bytes.

The test is whether the failure occurred before or after the identity's claim over the bytes was established.

5.8 Referrer

Clients MUST NOT send a referrer on cross-origin navigation from a derived origin.

Per-name origins make the referrer a disclosure of which name the user came from, at finer granularity than the web's, and there is no legacy content depending on the header. Fixing this by default costs nothing; leaving it to a per-site header repeats the web's mistake of making the safe setting opt-in.

5.10 Naming layers

This design is a two-layer naming system in the sense of Stiegler's petname model, and the third layer is deliberately left to clients.

Layer Here Securely unique Globally unique Memorable Assigned by
Key DID Yes Yes No The system
Nickname Handle No Yes Yes The publisher
Petname (client-side, Section 5.11) Yes No Yes The user

The short name is not a petname. It is assigned by the publisher and scoped to the publisher's namespace, which makes it a second nickname, structurally a path segment, not a naming layer.

5.10.1 Scope of naming insecurity

All naming insecurity is in the first address segment.

Within a proven identity the short name is securely unique: rkey uniqueness is enforced by the repo (Section 3.3), so @x/docs unambiguously denotes one record. The difficulty in distinguishing @a.example/docs from @b.example/docs is entirely a handle problem. The second segment is sound and needs no additional layer.

A petname layer therefore need only cover identities, not names: a user petnames @portable.agency, and @portable.agency/blog inherits it.

5.10.2 Display

Wherever a client displays a name, a location field, a link, a history entry, a share target, a tab title, it MUST use whichever of these is available, in order:

  1. The user's petname for the identity, where one exists (Section 5.11).
  2. The identity's current handle, resolved rather than cached.
  3. Neither: state that the identity is unresolved, and offer inspection (Section 5.12).

Clients MUST NOT display a handle cached alongside a pinned origin. A pinned origin whose handle has since moved to another party would otherwise be labelled with a name that no longer belongs to its publisher, an impersonation the client itself performs.

A DID MUST NOT be the ambient display. It belongs to the key layer (Section 5.10), and cannot be read, remembered, or compared by the person looking at it. Substituting one on failure does not inform the user; it trains them to accept opaque strings where they are meant to recognise something, which is the habit confusable-name attacks rely on, and an attacker who can disrupt handle resolution installs that habit for free. Section 5.12 is where the DID is available to anyone who wants it.

Where a petname exists, resolution is not on the display path at all, which removes the cost of step 2: a resolution per display leaks browsing to the DNS resolver. Clients relying on step 2 SHOULD read the cached DID document's alsoKnownAs rather than re-querying DNS, and SHOULD revalidate on a schedule rather than per render. The DID document is already fetched during Section 3.2, so the common case costs nothing extra.

Goal 1 settles which name wins where they disagree: a label the user chose substantiates better than a handle whose current ownership the client must take on trust.

5.11 Petnames

Petnames are OPTIONAL and entirely client-side. Nothing in the record format, the lexicon, or the protocol changes to support them. This section exists because the layer below is built to receive them, and clients that skip it should know what they are leaving unused.

5.11.1 Purpose

Four concrete things, none of which the user gets from the handle alone.

A name that cannot be taken from them. Handles are rented domains that transfer, lapse, and get sold. A user who reads @portable.agency is trusting that whoever holds that domain today is who held it when they decided to trust it. A petname is theirs: it does not move when the handle moves, and a transfer shows up as "the site you call Portable now presents a different handle" rather than as nothing at all.

Recognition instead of verification. Without a petname, every return visit asks the user to re-read a handle and decide whether it is the one they meant, a comparison against a string they half-remember, which is exactly the comparison confusable characters defeat. A petname replaces that with recognition: they see the word they chose, or they do not. Nobody else can cause the word they chose to appear.

Names in their own vocabulary. Publisher-assigned names are constrained by what was available and by how the publisher thinks of their own work. A user can call something Tax stuff or Mira's recipes, labels meaningful in the user's life rather than in the publisher's marketing. This is not a security property, and it is probably why anyone would actually use the feature.

Display that works when the network does not. Resolution can fail: DNS is unreachable, a PDS is down, the user is offline. If neither a petname nor a current handle is available, the client states that the identity is unresolved (Section 5.10.2); the DID remains available through inspection (Section 5.12), not as the site's display name. A petname is local, so it displays the same on a plane as at a desk.

A handle is a name the user is given and must keep trusting; a petname is a name the user has, and substantiates without asking anything of anyone.

5.11.2 The origin is the binding target

A petname binds a user-chosen string to a securely unique local key. The derived origin (Section 5.1) already is one:

It is computed locally, collision-free, and stable across handle changes and transfers. A petname table is therefore a map from user string to derived origin, requiring no new identifier and no coordination with anyone.

Clients SHOULD scope petnames to the identity rather than to each name under it, per Section 5.10.1.

Synchronizing a petname table between a user's own clients is out of band. It is client configuration, like bookmarks or a keychain, and whatever a client already uses to move settings between a user's devices works here unchanged: the table is a map of short strings to derived origins, and derived origins are computed identically everywhere at Layers 1 and 2.

This spec deliberately does not define a record for it. Putting petnames on the wire would make a local, optional, user-controlled layer into a protocol feature, and would publish, to anyone reading the repo, the list of identities a user has chosen to name and what they call them. That is a disclosure the user gains nothing from, and it would break goal 5 (Section 1.1) by making the top layer depend on the bottom one.

Note the limit this leaves, so it is not mistaken for an oversight: a user on a client with no sync has no petnames on a second device, and falls back to handle display (Section 5.10.2) there. That is the same position bookmarks are in, and the same remedy applies.

5.11.3 Assignment

Section 5.4 already has the user promoting an origin from ephemeral to pinned by deliberate action. That is where the petname model places petname assignment: at the establishment of an ongoing relationship rather than at first sight.

Clients SHOULD offer to set a petname at the moment of pinning, and SHOULD default it to the identity's current handle so the common case requires no typing.

This sidesteps the standing adoption problem with petname systems: users will not perform a naming ceremony for their own safety. Here it rides on a gesture they already want for persistence, and the security benefit is a side effect of the storage benefit.

5.11.4 Scope of mitigation

Fixed for return visits. A confusable or homograph handle (Section 12) cannot collide with a label the user chose. A handle transfer (Section 13.3) does not silently relabel a pinned site. Cross-name links (Section 5.9) can show a petname where one exists and flag its absence unmistakably where none does.

Not fixed: first contact. A user handed @portable.agency/blog with no petname for it is in exactly the position Section 13 describes, and that is where homographs bite hardest. Petnames defend established relationships, not introductions; no petname system has solved this and this one does not either.

Section 5.4's ephemeral tier already communicates "you have no relationship with this yet," and a petname makes the second visit safe. That is a real improvement over the web, where the second visit is exactly as unsafe as the first, but it is not a solution to phishing.

5.11.5 Composition

The layers compose because each is separable:

  • Key without nickname. A DID with no resolvable handle still resolves, loads, and gets an origin. Without a petname, the client states that the identity is unresolved (Section 5.10.2). The DID is available through inspection (Section 5.12), not as the display name.
  • Nickname without petname. The default today. Everything works; display depends on resolution and inherits DNS's weaknesses.
  • Petname without nickname. A user who petnames an identity whose handle later stops resolving keeps a working, well-labelled site. Arguably the most robust configuration, and unreachable on the web.
  • Petname without the protocol. A petname table over derived origins is meaningful even to a client that supports no handlers at all (Section 10.1, Core profile).

No layer depends on the one above it, which is what makes the petname layer safe to leave optional and safe to add later.

The layers can also be carried by different things: layers 1 and 2 travel over the protocol, the petname layer over whatever the client already uses for its own configuration (Section 5.11.2). Forcing every layer through one channel would put the user's private vocabulary on the wire.

5.12 Inspection

Goal 1 (Section 1.1) says a name must not claim more than it can substantiate. Its corollary is that a person MUST be able to see what a name does substantiate.

Clients MUST provide an inspection surface, reachable from wherever a name is displayed, that reports at least:

Identity Petname if set, current handle, verification status of the handle↔DID check (Section 3.2), and the DID
Name The short name, and the route record it resolved to
Disposition load or redirect, and for a load, which proof mechanism (Section 4.3.1)
Handler Which handler rendered it, by NSID (Section 4.2)
Freshness The commit rev and content CID this origin is pinned to (Section 5.2)
Tier Ephemeral or pinned, and what that grants (Section 5.4)

This is where a DID belongs. Inspection is requested, not ambient: the person asked, so an unreadable ground-truth identifier is the right answer rather than clutter in a place meant for recognition (Section 5.10.2). The analogue is the certificate viewer behind a browser's lock icon, nobody reads it in passing, and it must be there.

Clients MUST make inspection available for an unresolved identity, since that is the state in which a person most needs it, and MUST NOT require network access to report anything already known locally.

Clients SHOULD report the same information for a redirect target (Section 4.3.2) before activation, which is what Section 5.9.3's inspectable-target requirement asks for.

This section constrains what is reported, not how. A panel, a detail view, a command output and a log line are all conforming.

5.13 Labels

Labels are the only revocation channel in this design (Section 13.3). They are third-party assertions about a subject, published by a labeler and consulted by a client.

5.13.1 Labels are signed claims

A label names its source DID, its subject, a value, and a creation time, and carries a signature when it passes between services. Clients MUST verify that signature against the labeler's DID using the machinery of Section 3.

Structurally a label is the same kind of object as a route record: a signed assertion by a named party, verified the same way, and worth exactly what its author is worth. Consuming labels requires trusting no unauthenticated service.

5.13.2 Subjects

A label targets an account DID, a record AT-URI, or, with a content identifier, one specific version of a subject.

The last is worth more here than in a feed. Because content is immutable and content-addressed, a label naming a CID is portable and permanent: it applies to those bytes under any name, published by any identity, indefinitely. Labelling a DID or a route record blocks a location; labelling a CID blocks the content itself, with no successor name to chase.

This is stronger than the web's arrangement, where blocking follows names and names are cheap.

5.13.3 Two families

Clients MUST distinguish two kinds of label value, because they need opposite resolution rules.

Safety labels assert that loading the content is harmful: malware, credential phishing, abuse material. The most restrictive assertion from any subscribed labeler wins, and the check happens before load (Section 5.13.4). A user who subscribed to a labeler wants it to be able to stop them.

Preference labels describe content so a person can decide: adult material, subject matter, spoilers. These resolve from per-value user settings and MUST NOT give a single labeler a veto.

Collapsing the two produces either a censorship channel, where any subscribed labeler can unilaterally suppress a competitor, or a safety mechanism too weak to stop anything. Which values fall in which family is a labeler's declaration and a user's setting, not this specification's to enumerate.

5.13.4 When labels are consulted

Clients MUST consult safety labels before load, against a locally held set, and MUST NOT query a labeler per navigation.

Both halves are required. Consulting only at promotion (Section 5.4) never fires for a drive-by visit, which is the case a phishing label exists to stop. Querying per navigation discloses browsing to the labeler, turning the revocation channel into a surveillance channel.

A local set satisfies both: the decision costs no network call, and the labeler learns only that a client synced. Clients SHOULD sync over the label subscription stream rather than polling, and MAY backfill on first subscription.

Preference labels MAY be consulted at promotion or per user setting, since their outcome is presentational.

A label arriving after content is cached MUST take effect on the next load. Content already read has been read, which is the limit Section 13.3 records for revocation generally.

5.13.5 Expiry and negation

The proof chain does not depend on the device clock (Appendix A.2). Label expiry would introduce a clock dependency, so:

  • Negation is the retraction mechanism. A later label from the same source, with the same subject and value, marked as a negation, retracts the earlier one. It asserts that the original should be disregarded, not that its inverse holds.
  • Expiry is advisory. A client MAY ignore it, since ignoring it leaves a block in place and fails safe. A client MUST NOT treat an unexpired label as expired, since a skewed clock would then fail open.

5.13.6 Attribution and override

Clients MUST name the labeler responsible whenever a label changes what a person sees. Presenting a third party's assertion as the client's own finding is the same defect as presenting link text as a name (Section 5.9.3).

Clients MAY allow a user to proceed past any label, MUST NOT do so silently, and SHOULD scale the friction to the family: a preference label is a setting, a safety label is a deliberate act.

5.13.7 Availability and subscription

Clients MUST NOT fail closed on an unreachable labeler. They serve from the last known set and SHOULD report that it is stale.

Failing closed lets a labeler outage stop browsing. Discarding the set when the labeler is unreachable would let a network attacker unblock everything by blocking one service. A local set degrades to staleness, which is the correct failure.

No labeler is mandated. Zero subscribed labelers is a valid configuration, and means the client has no revocation channel. Clients MAY ship defaults and MUST allow them to be removed. Requiring a particular labeler would rebuild the central authority the rest of this design removes.

5.13.8 Self-labels

A publisher may embed label values in their own records, authenticated by the repo commit rather than a separate signature.

Self-labels are additive only. A client MAY apply them, MUST NOT treat their absence as evidence of anything, and MUST NOT let one weaken a label from a subscribed labeler. They are publisher-controlled content, subject to the same caution as link text (Section 5.9.3).

5.13.9 Labels are not takedowns

The two are separate and neither substitutes for the other.

Takedown (Section 3.2.2) Label (this section)
Asserted by The account's host Any labeler
Means This host will not serve it This party asserts something about it
Scope That host only Whoever subscribes
Effect on load Refuse Depends on family and settings
Effect on storage None; never erases None
Evaded by Moving to another host Unsubscribing

A takedown is enforcement by a party that was serving the content. A label is advice from a party that may never have hosted anything. A client applies both, and an intermediary serving a compatibility surface (Section 10.3) applies its own, which is one reason such a gateway is not neutral.

6. Discovery and subscription

Resolution (Section 3) answers where a name goes. Discovery answers what names exist. This section constrains client behaviour, because discovery is where a person first meets a name they did not already know, the case Section 13.1 lists as unprotected. It does not specify an index.

6.1 Local first

Completion sources, in the order a client SHOULD prefer them:

  1. Petnames (Section 5.11), the user's own vocabulary, and the reason typing Mira can reach a site whose handle they never memorised.
  2. Pinned origins (Section 5.4), relationships the user has already established.
  3. History.
  4. Enumeration within an identity the user has already named (Section 6.2).
  5. A directory or index (Section 6.3).

Clients MUST NOT transmit partial input to a third party by default. Each keystroke sent to a remote suggester discloses browsing in progress, including input never submitted and text typed by mistake. Sections 5.9.2 and 4.10.2 refuse the same disclosure for links and display.

Remote suggestion MUST be opt-in, MUST name the service it queries, and MUST be disableable without disabling local completion. Sources 1–4 need no third party at all.

6.2 Enumeration within an identity

com.atproto.repo.listRecords over the route collection returns every name an identity publishes. One request, no index, no AppView, and the repo already guarantees the names are unique within it (Section 3.3).

This is the natural response to a trailing separator, a person who has typed @b.example/ is asking what is there, and it is the directory Section 4.7 permits when a root name has no self record. As that section requires, such a listing is client chrome: it MUST NOT load into a derived origin and MUST NOT be presented as the identity's site.

The request goes to that identity's PDS, which learns of the interest. That is the same disclosure as visiting (Section 13), made slightly earlier.

6.3 Directories

Discovery does not require a global index. Three shapes exist, with different trust and privacy properties:

Source Covers Notes
A PDS The identities it hosts (com.atproto.sync.listRepos) Any PDS is a directory of its own community, self-hosted ones included
The DID directory Identities under a given DID method Global for did:plc; enumerable in bulk rather than queryable by prefix
A third-party index Whatever it chose to index May rank, search full text, and decide what is seen

The first matters most: because a PDS enumerates its own accounts, a community that runs a server is a browsable neighbourhood without anyone operating a global service. Clients SHOULD allow a user to add a PDS as a suggestion source.

A client MUST attribute a suggestion to the source that produced it. A service that ranks decides what people encounter, and an unattributed suggestion hides who made that decision.

6.4 Suggestions are proposals

A suggestion is a claim by whoever produced it. It is not a resolution and MUST NOT be presented as one.

A suggester can place @burrit0.space in front of someone who never typed it, and nothing here prevents that. These rules ensure it arrives labelled as a proposal from a named source rather than as something the client has vouched for.

6.6 Subscriptions

A subscription is a list of names a person wants to hear about, plus a change signal. Both already exist: the list is client state, and the signal is the (rev, contentCID) pair the floor already tracks (Section 5.2). No new record type is required, and this specification defines none.

Subscriptions are client-side by default. A list of what a person reads is a reading history. Publishing it as records would put that history on the firehose for anyone to index, which is a disclosure the subscriber gains nothing from, so publishing a subscription MUST be an explicit act with the consequence stated. The default is the same position petnames take (Section 5.11.2): local state, no wire format, synchronised between a person's own clients out of band.

6.6.1 Change signals

Source Cost Discloses
Firehose filtered by collection All route writes network-wide Nothing about which names are followed (Section 3.4)
com.atproto.sync.getLatestCommit per identity One cheap request per identity, no CAR To each publisher, that someone follows them
A third-party notification service One connection The entire subscription list to one party

Clients SHOULD prefer the firehose where bandwidth allows, since it is the only option that reveals nothing, and SHOULD fall back to per-identity polling rather than a third-party service. A third-party notifier MUST be opt-in and MUST name itself.

rev is a maybe; the content CID is the answer. A repo's revision advances on any write, including writes unrelated to any site (Section 5.2.1), so a changed rev means only that the route record is worth re-fetching. A changed switching record CID means the site actually changed. Polling is therefore one cheap request plus an occasional confirmation.

6.6.2 Notification is not content

A notification MAY be derived from unproven data. Content MUST NOT be.

The firehose carries no proof (Section 3.4), but the consequence of a false notification is only a wasted check, so a client MAY act on a stream event to decide that something is worth fetching. It MUST then fetch with proof before displaying anything as content, and MUST NOT present a title or excerpt taken from the stream as the publisher's words.

6.6.3 Background behaviour

  • A background check MAY advance the floor, but only if it performed full verification (Section 3.3). A check that merely observed a stream event MUST NOT.
  • Safety labels MUST be consulted before notifying (Section 5.13.4). A notification is an invitation to load, and inviting someone to load something a label covers defeats the check.
  • A subscription to a retired name (Section 4.1.1) SHOULD notify once and then stop.
  • Clients SHOULD rate-limit notifications per name. A publisher can write a switching record as often as they like.

6.7 Item-level subscriptions

Route records name sites, not items, so a subscription to a name yields "this changed" and nothing finer. Finer granularity needs an item vocabulary, and standard.site already is one (Section 14.4).

Where a name's selected target is a site.standard.publication, its document records are the items. Each carries a title, a publication time and a path, and that path is what Section 3.1.4 passes to the handler, so a document becomes an address with no additional mapping:

The route record supplies the name, standard.site supplies the items, and the path field joins them. Neither vocabulary needs to change.

Granularity follows the target. An inclusion-proven document target yields an item feed. A content-addressed target yields site-level change only, because a manifest records files rather than articles and a changed file is not an article. This is a further reason for the recommendation in Section 9.9 that longform publishers emit standard.site records alongside their route records: it is what makes their work subscribable at the level a reader cares about.

Items obtained this way are subject to Section 6.6.2. A document record proven by MST inclusion is content and may be displayed; the same record seen on the firehose is a notification until proven.

RSS is not a competitor here. A publisher may offer both, and a client may consume a feed from an asserted target under ordinary web rules (Section 4.3.2).

7. Gateways

A conforming client needs no gateway. DID document → serviceEndpoint → com.atproto.sync.getRecord and com.atproto.sync.getBlob. No gateway, no AppView, no relay. CORS does not apply because a protocol handler is not a web origin.

Gateways remain useful for three things, none of which is authority:

Load. Every visitor fetching every blob from one PDS makes a popular site a sustained load on its owner's server, which may be small and which has rate limits set for unrelated reasons. The failure mode is that success takes the site down and the owner's posting with it.

Compatibility. Anyone without a conforming client, initially, everyone, needs an HTTPS URL.

standard.site verification. The <link rel> handshake needs a reachable HTTPS document (Section 14.4).

A gateway serving provable content is a cache of content-addressed bytes the client re-hashes anyway. It cannot lie, needs no trust, and any number may exist. This matches wisp's stated posture exactly: hosting services are read-only caches that can always reconstruct from the repo (Section 14.1). Direct clients reuse the same cache → mirror → PDS chain with the same verification, and simply start further down it.

The correct summary is that this spec demotes gateways from authority to CDN. It does not eliminate them.

8. Limits

Every limit below is a MUST for clients. Publishers exceeding one produce content that conforming clients refuse.

Limit Rationale
Targets per record 16 Bounded resolution work per name
Redirect hops 5 Section 4.5's cap, made concrete
Alias hops 0 Aliases never chain (Section 4.7)
Manifest entries 10,000 Bounds validation and index memory
Resource size Handler-defined Must be declared, not implicit
Decompressed size 100× declared, 256 MB absolute See below
Concurrent blob fetches per load 8 Protects the publisher's PDS (Section 7)
Mirrors per target 8 Bounded retry work
Short name length 512 atproto record key ceiling
Address length 2,048 Bounded parsing; well above any legitimate name
Floor table entries 10,000 Section 5.2.2's bound, made concrete

Decompression. Content addressing proves the compressed bytes and says nothing about their expansion ratio. A small, validly proven blob can expand without bound. place.wisp.fs compresses by design (Section 14.1), so this is live, not hypothetical. Clients MUST enforce both a ratio cap and an absolute ceiling, and MUST abort mid-stream rather than after, since a post-hoc check has already allocated the memory.

Aggregate limits matter as much as per-item ones. A manifest of 10,000 entries each just under the resource ceiling is within every individual limit. Clients SHOULD enforce a total-bytes budget per load and fail closed when it is exhausted.

Clients MUST NOT accept a publisher-supplied value that loosens a limit, that is Section 5.3's narrowing-only rule applied to resources rather than capabilities.

Clients SHOULD NOT impose stricter limits. These are exact, not floors; halving a ceiling makes conforming content fail on one implementation and succeed on another. Where resource constraints force one, the client MUST report it as a client limitation (Section 10.2).

9. Publishing

The rest of this spec is read-side, written against a hostile network. This section is the write side, and its governing observation is that the write path has been designed against a cooperative one. It assumed a single publisher, publishing quickly, in one commit, to a PDS with no limits. None of those hold.

Items marked [verify] rest on PDS implementation behaviour that should be confirmed against multiple running implementations before being treated as settled. Section 1.3's posture applies most directly here: every mitigation below is written to be safe across the range rather than tuned to one implementation, so verification should change the tuning of these rules, not their existence.

9.1 Record and blob atomicity

A commit is atomic. Blob upload is not part of any commit, a blob uploaded via com.atproto.repo.uploadBlob exists before anything references it.

This creates a window in which uploaded blobs are unreferenced, and PDSes reclaim unreferenced blobs. [verify] the reclamation policy and timing of target PDS implementations; the hazard's existence is structural, but its window is not specified by atproto.

The consequence is size-dependent and silent: a small site uploads and commits within seconds and never notices, while a large site can have its earliest blobs reclaimed before the record referencing them lands. It presents as flaky hosting, not as an error.

9.2 Publishing sequence

Publishers MUST land the switching record last.

  1. Compute locally. Build the manifest and every CID before contacting the PDS. Content addressing means the entire artifact is known before any upload.
  2. Skip what exists. For each blob, attempt retrieval; upload only on miss. This is what makes publishing resumable (Section 9.4).
  3. Upload blobs, anchoring them as you go (Section 9.3).
  4. Verify retrievability. Fetch back every blob the manifest names. A publish that cannot be read is not a publish.
  5. Write the switching record (Section 4.2) in one commit, last.

Nothing references new content until step 5, so a publish that fails at any earlier step is invisible to readers rather than half-visible. This is what gives a deploy readiness semantics without adding a readiness concept to the protocol.

Publishers SHOULD verify their own deploy through a conforming read path between steps 4 and 5. It is cheap, it runs against the same proofs readers will use, and it catches manifest errors while they are still unpublished.

Do not rewrite an unchanged record. If the switching record's content is byte-identical to the current one, its CID is identical and the write accomplishes nothing except advancing rev. Given Section 5.2, needless rev churn is not free. Publishers SHOULD compare before writing.

9.3 Staging records anchor blobs

The window in Section 9.1 is closed by referencing blobs as they upload rather than only at the end.

Publishers SHOULD write staging records: small records, in a collection no handler resolves, that reference each batch of uploaded blobs. A staging record's only job is to keep its blobs referenced.

Each staging record is small enough to commit comfortably, blobs are never unreferenced for longer than one batch, and no reader ever sees a staging record because no handler resolves that collection. The torn state exists in the repo and is invisible through the name.

Staging records also give a resumed publish (Section 9.4) a record of what already uploaded, without needing to probe every blob.

Publishers SHOULD clean up staging records after the switch, and clients MUST ignore them, a staging collection is not a target.

9.4 Large sites, commit ceilings, and rate limits

com.atproto.repo.applyWrites bounds how much can land in one commit [verify], and PDSes rate-limit writes per hour and per action [verify] the current values. A site with many files is many blob uploads, and publishing may simply not complete in one pass.

Two consequences the spec must own:

Atomicity survives, but only through the switch. A publish spanning many commits is still atomic at the name, because Section 4.2 requires the handler to resolve through one record. Appendix B's advice to use applyWrites for one logical update holds for updates that fit; beyond that, the switching record is the mechanism, not the commit.

This is a real advantage of deep CAS over shallow signing, and Appendix B undersells it: one manifest record switches a site of any size, whereas per-resource signing needs a generation pointer to get the same property.

Publishing MUST be resumable. Content addressing makes this nearly free, step 2 of Section 9.2 skips what already exists, so a publish interrupted by a rate limit resumes by re-running from the top. A publisher that cannot resume will, on a large site, leave blobs unreferenced across a multi-hour backoff, which is Section 9.1's hazard held open for exactly as long as it takes to matter.

Publishers SHOULD back off on rate-limit responses rather than retrying tightly, and SHOULD prefer fewer, larger blobs where the handler permits, since limits are generally per-action rather than per-byte [verify].

9.5 Propagation and reverse skew

Section 5.2 handles mirrors lagging behind the origin. The reverse also happens: the switching record commits, and mirrors do not yet hold the blobs it names. Readers get a proven, current manifest naming content nobody can serve.

This self-heals given the rest of the spec: mirrors are advisory hints (Section 4.1), and retrying the same CID from another source, including the origin PDS, is a transport choice, not a fallback (Section 4.4). A mirror miss is a cache miss.

Publishers SHOULD allow mirrors to pre-warm between Section 9.2's steps 4 and 5, since blobs are retrievable from the origin before the switch flips. Publishers MUST NOT treat a mirror as the sole source of any blob: a mirror that has not caught up would then make a proven site unloadable.

9.6 Retirement and deletion

Retiring is not deleting (Section 4.1).

This is accepted, not solved (Section 13.3). It is the same hazard as re-registering a lapsed domain and inheriting its cookies and localStorage, which the web has always had. Making it safe by construction would mean putting something mutable into Section 5.1's derivation, and Section 5.1 is the one part of this design that cannot change later without destroying every site's storage. Discipline plus a warning is the cheaper trade.

9.7 Concurrent writers

Nothing in atproto prevents two agents holding credentials for one DID from writing the same route record. An organizational handle with several people, or a publisher running two deploy pipelines, is an ordinary multi-writer conflict on an rkey.

Who may hold credentials for a shared identity is an atproto question, not this spec's. The conflict mechanics are this spec's, because the failure mode, orphaned blobs plus a rev advanced past content that was never switched to, is specific to how publishing works here.

9.8 Key handling

Section 5.2 makes rotation the mechanism by which a publisher recovers from signing key compromise, and the guard on that reset is the DID recovery window. Two obligations follow:

A publisher without rotation-key access can still publish. They cannot recover.

9.9 Publisher requirements

A conforming publisher MUST do all of the following.

  1. Land the switching record last, after all blobs are uploaded and verified retrievable (Section 9.2).
  2. Make publishing resumable (Section 9.4).
  3. Use swapRecord when updating a record that was read (Section 9.7).
  4. Keep rotation keys out of deploy pipelines (Section 9.8).
  5. Declare a content type for every resource (Section 5.5).
  6. Write cross-name links in link form, never as platform origins (Section 5.9.1).
  7. Store manifest paths in canonical form (Section 5.6).
  8. Stay within Section 8's limits.
  9. Not treat a mirror as the sole source of any blob (Section 9.5).

A conforming publisher SHOULD do all of the following.

  1. Anchor blobs with staging records during long publishes (Section 9.3).
  2. Verify the deploy through a conforming read path before switching (Section 9.2).
  3. Skip writes that would not change the record's CID (Section 9.2).
  4. Write all records for one logical update in a single commit where it fits (Section 3.4).
  5. Not fingerprint filenames. style.a3f9c2.css defeats cross-version dedup by giving identical bytes different paths, and the caching it buys is already provided by content addressing (Section A.2). Every static-site toolchain does this by default and it must be turned off.
  6. Publish manifests as records rather than blobs, so the pointer stays legible and labelable even when the content is opaque (Appendix B).
  7. Emit site.standard records alongside route records for longform content (Section 14.4), which is what makes it subscribable per item (Section 6.7).
  8. Retire names rather than deleting them, and not reuse retired names (Section 9.6).
  9. Use link text that is either the target's name in display form or plainly not a name (Section 5.9.1).

A conforming publisher MUST NOT do any of the following.

  1. Rely on robots.txt to withhold anything. Records are on the firehose regardless (Section A.2).
  2. Assume a gateway URL will remain reachable. It is a cache, not an address (Section 7).

10. Client conformance

A conforming client MUST do all of the following.

  1. Verify handle↔DID bidirectionally, attempting both resolution methods and preferring DNS on disagreement (Section 3.2).
  2. Support did:plc and did:web, selecting DID document entries by id fragment (Section 3.2.1).
  3. Resolve records via com.atproto.sync.getRecord and verify commit signature and MST inclusion (Section 3.3).
  4. Determine each target's disposition and report it (Section 4.3).
  5. Assign disposition independently of viewer identity or authentication state (Section 4.3).
  6. Select one target and fail hard rather than fall through (Section 4.4).
  7. Select the first provable target supported; select an asserted target only if no provable target is supported (Section 4.4).
  8. Cap redirect hops across record types (Section 4.5).
  9. Show the destination origin, not the name, for redirects (Section 4.3).
  10. Resolve @<handle> and @<handle>/self identically, to the same origin (Section 4.7).
  11. Resolve aliases before deriving an origin, and reject chained or cross-identity aliases (Section 4.7).
  12. Give each (identity, name) a distinct origin, derived only for load targets (Section 5.1).
  13. Enforce monotonic rev per origin (Section 5.2).
  14. Reject entries escaping the origin; treat publisher policy as narrowing-only (Section 5.3).
  15. Withhold persistent storage and permissions from unpinned origins (Section 5.4).
  16. Take content types from the manifest or record, and never sniff (Section 5.5).
  17. Follow the handler's path-resolution rules rather than applying its own (Section 5.6).
  18. Render pre-proof failures as client chrome, never as publisher-supplied content (Section 5.7).
  19. Suppress the referrer on cross-origin navigation from a derived origin (Section 5.8).
  20. Normalize paths once, and use the same normalized form for the escape check and the lookup (Section 5.6).
  21. Resolve cross-name links lazily, never as a side effect of rendering (Section 5.9).
  22. Record a rev floor for every origin, ephemeral included, and honour a rotation reset only after the recovery window (Section 5.2).
  23. Never erase origin storage in response to an account-state signal (Section 3.2).
  24. Reject javascript: everywhere and data: as a script source (Section 5.3).
  25. Fail closed on unrecognized policy keys listed in critical (Section 4.1).
  26. Accept a proof whose content CID matches the floor regardless of its rev, and apply the rev comparison only when content differs (Section 5.2).
  27. Obtain proof from com.atproto.sync.getRecord, never from a simplified firehose stream (Section 3.4).
  28. Accept display form and link form as equivalent, and never treat an origin as a link (Section 3.1).
  29. Make a link's target inspectable before activation, and never present link text as agreeing with a target it does not name (Section 5.9.3).
  30. Display a name using the petname, then the resolved handle, and otherwise state that the identity is unresolved rather than showing a DID (Section 5.10.2).
  31. Provide an inspection surface reporting identity, disposition, proof, handler, freshness and tier (Section 5.12).
  32. Consult safety labels before load against a locally held set, never querying a labeler per navigation, and attribute any label that changes what a person sees (Section 5.13).
  33. Keep completion local by default, and treat every suggestion as a proposal: resolved in full on activation, never presented as verified, never pinning, never borrowing a petname (Section 6).
  34. Treat a stream event as notification rather than content, advance a floor from a background check only on full verification, and never publish a subscription list without an explicit act (Section 6.6).
  35. Distinguish an absent record from a retired one, and never auto-follow a successor (Section 4.1).
  36. Ignore staging collections; a staging record is not a target (Section 9.3).
  37. Fail closed on any verification mismatch. Never serve unproven bytes into a derived origin.

A conforming client MAY support any subset of handlers, including none beyond redirect. See Section 10.1.

10.1 Baseline profile

Handler support is open-ended (Section 4.2), so a ladder of profiles would be unsound: no level could be complete, and the top of one would mean different things in different implementations. There is one profile and a declared capability set instead.

Core is the baseline and is mandatory. It comprises every requirement in Section 10 plus redirect handling, and loads nothing itself. A conforming reader, browser extension, indexer, or crawler can be Core and remain useful: it resolves names, verifies identity, applies Section 4.4's selection rules, and redirects with the disposition reported.

Anything beyond Core is a capability set (Section 10.2), not a higher profile. A client that implements one handler is not less conforming than one that implements five; it is conforming with a smaller set.

Two properties of Core make it a useful floor rather than a formality:

10.2 Capability declaration

A client's capability set is:

  1. Which proof mechanisms it implements. This set is closed: inclusion, content-address, or both (Section 4.3.1).
  2. Which handlers it implements, by NSID.
  3. Which non-at:// schemes it can load, if any.

Unlike the handler list, the proof-mechanism set is closed and therefore meaningful to compare between implementations. Two clients supporting different handlers but the same mechanisms have the same security properties and differ only in reach.

Clients MUST document their capability set. Clients MUST state the distinction when a name fails for lack of a handler: this client cannot load this site and this site is broken are different statements, and Section 4.4 forbids conflating them by falling through.

Clients SHOULD name the missing handler when reporting such a failure, so a user can find a client that does support it. Clients MUST NOT represent an unsupported handler as a defect in the publisher's content.

10.2.1 No client-server negotiation

There is no negotiation in the HTTP Accept sense, and none is possible. The route record enumerates targets; the client selects locally under Section 4.4. A PDS serves records and blobs and has no knowledge of, or influence over, which target a client chose.

This is a consequence of the layering rather than an omission. Selection is a security decision (Section 4.4), and a security decision delegated to a remote party is not one. A negotiation protocol would give the serving side influence over which disposition the client ends up with, which is the downgrade Section 4.4 exists to prevent.

What exists instead is one-directional: publishers declare targets in preference order, clients select from what they can prove and load. The publisher expresses intent; the client decides.

10.2.2 Discovery is not support

An unknown handler NSID is resolvable, atproto NSIDs resolve to lexicons, so a client can discover the shape of records it has never seen.

This does not confer support. A handler is code, not a schema: knowing a record's fields says nothing about how its content is assembled, verified, or rendered. Clients MUST NOT attempt to load a target whose handler they do not implement, however well they can parse it.

Support is therefore always explicit, and adding a handler is a client release rather than a runtime discovery.

10.3 Intermediaries and the compatibility surface

An intermediary serving com.atproto.sync.* to conforming clients (Section 11.2) performs no selection. The client resolves, selects, and verifies; the intermediary only moves bytes. No capability declaration is involved.

An intermediary serving a compatibility HTTP surface to non-conforming browsers (Section 7) is a different matter: it selects a target on the browser's behalf, because the browser cannot.

Such an intermediary:

A browser reaching content this way has none of Section 5's guarantees: the origin is the gateway's, verification happened elsewhere, and the browser trusts the gateway as it would any web server. The guarantees stop at the gateway.

11. Implementation notes

The resolver SHOULD be a pure function with transports injected. DNS, HTTP fetch, cache, clock. Everything in Section 3 and Section 4 is then shared across platforms and testable without a network.

Platform Origin mechanism Notes
Electron registerSchemesAsPrivileged + protocol.handle standard, secure, supportFetchAPI, stream. Not bypassCSP. Separate session partition; no preload.
iOS WKURLSchemeHandler Custom schemes are not secure contexts without additional work
Android WebViewAssetLoader Maps onto a virtual https:// origin; a different model; verify the property still holds

Platform caveats to settle up front:

11.1 PDS serving characteristics

com.atproto.sync.getBlob returns whole blobs. There is no range support, no conditional request, no compression negotiation, and rate limits set for a PDS's normal workload rather than for serving a popular site.

Consequences for implementers:

11.2 Intermediaries

An intermediary sitting in front of one or more PDSes SHOULD expose com.atproto.sync.* rather than inventing a protocol. A conforming client then points at a different base URL with no new code path, and verification is what makes that safe, the intermediary is an atproto read-path cache, not a trusted party. A compatibility HTTP surface for non-conforming browsers (Section 7) is a second face on the same store.

Intermediaries MUST coalesce concurrent upstream requests. Section 8 caps a client at 8 concurrent blob fetches, which does nothing for an intermediary: a cold cache with ten thousand concurrent readers becomes ten thousand upstream requests against one publisher's VPS. That is exactly the failure Section 7 exists to prevent, caused by the thing meant to prevent it. Single-flight per CID, per record, and per identity resolution.

An intermediary's job is to be one well-behaved client to the origin PDS on behalf of many readers.

12. Test vectors

Two independent implementations are expected, so conformance should rest on shared fixtures rather than on prose agreement: records plus identity state in, expected outcome out. Adversarial cases are the point.

Each vector below is keyed to the section it exercises. A conforming client should produce the stated outcome for every vector covering a section it implements.

12.1 Identity resolution (Section 3.2)

12.2 Name resolution (Section 3.3)

12.3 Record validity (Section 4.1)

12.4 Retirement (Section 4.1.1)

12.5 Selection and dispositions (Sections 4.3–4.5)

12.6 Root names and aliases (Section 4.7)

12.7 Origins and rollback (Sections 5.1–5.2)

12.8 Loading (Sections 5.3, 5.5–5.6)

12.9 Limits (Section 8)

12.10 Presentation (Sections 5.9–5.10)

12.11 Address forms (Section 3.1)

12.12 Labels (Section 5.13)

12.13 Discovery (Section 6)

12.14 Subscriptions (Section 6.6)

12.15 Publishing (Section 9)

13. Security

13.1 Threat model

Trusted: the client and its update channel, the DID method's own guarantees (log integrity and the recovery window), the cryptographic primitives, and the user's device.

Not trusted: every network participant, including the publisher's own PDS, every mirror and intermediary, DNS, suggestion services, indexers, and labelers.

Protected: integrity of provable content against everyone except the holder of the identity's signing key; namespace exclusivity within an identity; origin isolation between names; freshness against rollback; and honest naming, meaning the client never presents a claim it cannot substantiate.

Not protected: reader anonymity (Section 13.2), unpublishability, availability, confidentiality (Section 16), and recognition of a confusable name on first contact (Section 6.4).

Out of scope: compromise of the user's device, traffic analysis beyond the readership disclosure named below, and any guarantee that content remains reachable.

Adversary Can Cannot See
PDS operator Withhold, rate-limit, serve stale, misreport account state, observe readership Forge a commit, alter a record undetected, substitute bytes, erase client storage Sections 3.2.1, 7
Mirror or intermediary Withhold, serve superseded content, observe readership Substitute bytes, promote itself to authority Sections 4.4, 7
On-path network Block, delay, tamper with asserted targets Alter provable content Section 4.3
DNS or handle layer Break handle resolution, register confusables Make a DID claim a handle it does not Sections 3.2, 13.2
Handle successor Take the label Take origins, storage, or past content Section 5.10
Signing key holder Publish anything under that identity's names Roll back below the floor until a rotation settles Section 5.2
Rotation key holder Take over the identity; reset the floor and roll back Act inside the recovery window unopposed by a higher-priority key Sections 5.2.3, 9.8
Publisher Impersonate in link text, escape paths, widen policy, ship decompression bombs, inject non-manifest scripts Exceed a limit, have unproven bytes served, borrow another identity's origin Sections 5.3, 5.9.3, 8
Suggester or index Propose a confusable, rank, observe typed input Present a suggestion as verified, borrow a petname, pin an origin Section 6.4
Another loaded site Nothing across names Reach another name's storage or scripts Section 5.1
Labeler Propose labels; suppress content for its subscribers Compel a client, recall content already read, act unattributed Section 5.13

Two properties fall out of the table. Every adversary that can withhold is unable to substitute, which is what makes hosting a performance decision rather than a trust decision (Section 7). And the publisher is an adversary: most of Section 5's rules defend the person reading a site against the person who published it.

13.2 Considerations

Identity takeover inherits everything. Compromise of a DID's rotation keys yields every origin and all accumulated storage under that identity. Roughly par with domain transfer, so not disqualifying, but it is the real argument for publisher key hygiene guidance.

Unbounded origin creation. Any identity can mint arbitrarily many origins at no cost. Section 5.4 is the mitigation; a client that skips it will have a quota problem.

Homograph and near-handle attacks. Handles are domains and inherit every confusable-name problem DNS has. Wherever a client displays a name, a location field, a link, a share sheet, a history entry, is the mitigation point, and a client with no location field has more of them to get right, not fewer.

Petnames (Section 5.11) close this for return visits, since a chosen label cannot be collided with. They do not close first contact, which is where confusables do their work, and Section 6.4 is the path that delivers most of it. Section 5.4's ephemeral tier is the partial answer there: an unpinned origin communicates that no relationship exists yet.

Note an asymmetry in normalization: short names are ASCII by record-key syntax, but handles are IDN and are not. Clients MUST apply IDNA processing to handles before display and before comparison, and SHOULD mark mixed-script handles distinctly. A client that normalizes for resolution but not for display shows one name while loading another.

Direct reads disclose readership. Section 7 removes gateways from the trust path, but gateway-mediated reading also hid readers from publishers. A client fetching blobs directly tells the publisher's PDS operator exactly who read what, and the publisher may operate that PDS themselves.

This is a real cost of the design, not an oversight, and it runs opposite to the usual decentralization intuition: the more direct the fetch, the worse the reader's privacy. Clients that care SHOULD fetch through a cache because a cache learns the reader's identity in the publisher's place, verification is unaffected either way, which is what makes the choice available at all.

Connection-origin decoupling. This intentionally breaks the implicit guarantee that bytes in an origin arrived over a connection authenticated to that origin, the objection Mozilla raised against Signed HTTP Exchanges (Section 14.6). That guarantee is replaced by proof to a signed commit, strictly stronger for provable targets and explicitly absent for asserted ones, which is why Section 4.3 separates them by disposition rather than grading them on one scale.

Revocation is advisory. Labels (Section 5.13) are a retraction channel, but clients choose their labelers and cached content survives. No content addressed this way can be assumed unrecallable.

13.3 Accepted limitations

Goal 7 (Section 1.1) permits deliberately accepting gaps already present in the HTTP web where addressing them would cost more than it buys. The comparisons below explain those trade-offs; they do not set a feature-parity goal or promise equivalent behaviour. The list also identifies limitations specific to this design, so each is argued once here rather than rediscovered as an oversight.

Problem Web equivalent Why accepted
Retired name reuse inherits storage (Section 9.6) Re-registering a lapsed domain inherits its cookies and localStorage The fix would put mutable state into Section 5.1's derivation, and that derivation cannot change later without destroying every site's storage
Readership disclosed to the publisher's PDS (Section 13) Server access logs Intrinsic to fetching from someone. Mitigable by fetching through a cache, which verification makes safe
No true revocation (Section 13) Cached pages, archives, mirrors Anything that reached a public log is out of the publisher's hands. Takedown is a request to stop, not erasure
Handle transfer moves the display name (Section 5.10) Domain transfer Storage and origins key on DID and survive it; only the label moves. Petnames (Section 5.11) remove it entirely for anyone who set one
Homograph and confusable handles at first contact (Section 13) IDN homograph attacks Inherited from DNS. Petnames close return visits (Section 5.11); first contact is unsolved everywhere
Identity compromise inherits everything (Section 13) Losing control of a domain Par with the web's worst case. Section 5.2 and Section 9.8 make recovery possible, which is more than a stolen domain offers
Key rotation grants a rollback channel (Section 5.2) Certificate reissuance after compromise The alternative is no recovery at all. Bounded by the DID recovery window
Unbounded origin creation (Section 13) Unbounded subdomain registration Section 5.4's ephemeral tier makes drive-by creation cost nothing durable
Per-platform origin strings, so storage does not migrate (Section 11) No web equivalent; strictly worse here Accepted because the alternative is DNS, which is what the design exists to avoid
Clients with differing capability sets reach different content (Section 10.2) Browsers with differing codec and format support Declared capability sets and explicit error messages, rather than silent degradation

Three problems are not accepted: substituted content, which is always a hard failure; silent downgrade, which Section 4.4 forbids even though the web permits it in several places; and client-performed impersonation, a stale handle, a link presented as resolved, an asserted target shown as proven, which is forbidden in every case.

That division is goal 1 stated as a tolerance, and it is the test for anything added later: does this let a name overstate itself? If yes it is a defect whatever it buys. If no it is a cost to weigh.

14. Relationship to existing work

14.1 wisp.place

wisp is the nearest existing system to this design, so the comparison is the most useful one in this section.

wisp.place This spec
Address sites.wisp.place/{handle}/{site} @handle/name, resolved to a derived origin
DNS per site None required None required
Namespace root Handle Handle for input, DID for everything durable
Uniqueness rkey within the repo rkey within the repo
Source of truth The author's PDS The author's PDS
Hosts are Read-only caches, reconstructable from the repo Read-only caches, reconstructable from the repo
Storage Files compressed into blobs (place.wisp.fs, place.wisp.subfs for chunking) Handler's business; place.wisp.fs is a registered handler
Invalidation Hosting services watch the firehose Same, but firehose is invalidation-only (Section 3.4)
Who verifies The hosting service; the reader trusts it over TLS The client, against the signed commit and CIDs
Origin isolation One shared origin for every publisher One origin per (identity, name)
Atomic switch Manifest record Required of every handler (Section 4.2)
Custom domains place.wisp.domain, optional Optional, and not the addressing mechanism
Private sites In wisp storage, off-PDS Out of scope (Section 16)
Self-hostable Yes Yes; capability sets, Section 10.2

Where they agree, they agree completely. PDS as source of truth, hosts as reconstructable caches, rkey-scoped names with no allocation registry, custom domains as optional decoration. Those were wisp's positions first and this spec adopts them without modification.

The two substantive differences:

Who verifies. In wisp the hosting service verifies and the reader trusts the host over TLS, a sound model, and the reason wisp works in any browser today. This spec moves verification into the client, which demotes the host to a cache that cannot lie (Section 7) but requires a conforming client to get any benefit. wisp's model reaches everyone; this one reaches fewer people with a stronger guarantee. Both are legitimate, and the gateway is where they meet.

Origins. wisp serves every publisher from one origin, so all sites share storage, cookies, and scripting access. Section 5.1 requires per-name origins. This is why a conforming client cannot simply proxy wisp's HTTP surface, it would inherit the shared origin, and why place.wisp.fs is registered as a handler that reads the records directly instead.

The alignment test: a wisp site should be nameable by a route record with no change to wisp. If that ever stops being true, this spec has a bug.

14.1.1 Detail

Borrowed: the framing of hosting services as read-only caches that can always reconstruct from the repo, which this spec adopts as its gateway posture (Section 7); firehose-driven cache invalidation (Section 3.4); and handle-namespaced, path-per-site addressing, which is the shape generalized here.

Also diverging: wisp's URL keys on handle, while Section 3.4 and Section 5.1 key durable state on DID, so a handle transfer does not hand the previous owner's storage to its new holder.

Out of scope rather than disputed: wisp's private sites live in wisp storage, never on the PDS or firehose, so they are unprovable by this spec's chain (Section 16). The two models therefore do not cover the same ground.

14.2 Isolated Web Apps

IWA packages apps into signed Web Bundles served from an isolated-app: scheme whose hostname is a Web Bundle ID: base32 of an Ed25519 public key plus a short type suffix. Transport is irrelevant to origin identity.

Borrowed:

Aligned: the premise that origin identity can be anchored in a signature rather than a connection.

Diverges, structurally:

14.3 pkarr

pkarr publishes signed DNS packets to the Mainline DHT under Ed25519 public keys, giving keys that resolve as sovereign TLDs, with HTTP relays for clients that cannot open UDP sockets.

Adopted: as an optional transport for DID documents (Section 3.2), reducing dependence on a single DID directory. Listed as redundancy, never required. A client with native networking can speak the DHT directly rather than through relays, which is where most of the decentralization claim otherwise leaks.

Not adopted, as the naming layer, for three reasons:

14.4 standard.site

standard.site (site.standard.publication, site.standard.document) gives publishers a shared vocabulary for longform content, with documents carrying path, metadata, and text content, verified by a <link rel> on the served page pointing back at the AT-URI.

The verification direction is inverted between the two specs, which is what makes them compose rather than compete:

standard.site This spec
Authoritative side The website The record
Delete the record Site still works Name ceases to exist
Proof direction URL proves the record Commit proves the bytes
Granularity Per document Per site
Audience Indexers, readers Clients navigating

Aligned / adopted:

Unresolved friction: site.standard.document expects an HTTPS URL. Content at a derived origin has none. Discovery still works, because indexers consume records from the firehose rather than crawling. But the <link rel> verification handshake needs a reachable document, so a route-only site is indexed but unverified, no publication badge. This is cosmetic rather than structural, and it is the one remaining reason a publisher might want a gateway URL they would not otherwise need (Section 7).

14.5 AT Tags

AT Tags is Chris Shank's draft proposal for metadata connecting web pages to atproto records and identities. This comparison describes the draft at commit a783376.

It defines four standard <meta name="at:..." content="..."> properties: at:canonical for the records a page represents, at:alternate for auxiliary records, at:author for the page's authors, and at:me for identities associated with the site or section. Properties may repeat; they describe multiple relationships, not a ranked list of destinations. Namespaced custom properties can describe additional relationships.

The directions are complementary.

AT Tags Atproto web names
Starting point A web page already reached A name such as @handle/blog
Resolves to Related record AT-URIs and identity AT-URIs A route record, a selected target, and its loading disposition
Main purpose Discovery, attribution metadata, embeds, and relationship verification workflows Name resolution, proof of loaded bytes, and origin isolation
Canonicality Which records the page represents Which target the publisher names; whether its bytes are provable
Origin and storage Not defined by the proposal Defined per identity and short name

AT Tags explicitly leaves translating an arbitrary AT-URI back into an app's web URL to other work. Atproto web names does not provide a general record-to-app URL registry either: its route collection names sites, and handlers interpret the targets selected from those routes. The two proposals address different parts of the navigation problem.

Metadata is not a content proof. A page can claim any author or canonical record in its tags. Discovering that claim does not prove that the named identity published the page's bytes. Tags on an asserted HTTPS target can supply the page-side claim for a reverse-binding check (Section 4.6), but do not change its redirect disposition or entitle it to a derived origin. Tags inside a proven page are authenticated as part of that page; claims they make about other identities still need their own verification. They do not replace the handle binding, signed commit, MST inclusion, or blob hashes required by this specification.

A route record is not necessarily the canonical page object. AT Tags uses deletion as a rule of thumb: removing a canonical record would also remove the page, while removing an alternate would not. Removing a route record removes the name; content hosted elsewhere may remain available. A page may therefore advertise its route as at:alternate while keeping its actual document record as at:canonical, where those relationships apply. Reaching a page through a route is not by itself a reason to label that route canonical.

The HTML choice is separate from the naming scheme. AT Tags uses <meta> partly to avoid the URL-syntax problems of putting existing AT-URIs in <link href> attributes, generalizing the sort of reverse metadata used by standard.site (Section 14.4). Storing an AT-URI as metadata text does not change its syntax or turn it into a navigation URL. This specification separately defines link form (Section 3.1.2); AT Tags neither settles the provisional atw: scheme name nor replaces that link form.

Publishers can support both proposals without changing the route format. AT Tags are optional discovery metadata, not a prerequisite for resolving a name or loading proven content. A client using them for cross-name navigation remains subject to Section 5.9's lazy-resolution rule.

14.6 Others

Signed HTTP Exchanges. The same origin/connection decoupling, and the source of the objection recorded in Section 13. This spec accepts it rather than answering it.

WebFinger links. Ordered typed pointers with the same downgrade and over-claiming failure modes. Relevant before Section 4 is frozen.

IPFS. CID-subdomain origins unify on content and lose storage on every publish; IPNS-key origins unify on name. Section 5.1 is the second shape with a DID where IPNS has a raw key, which is what makes rotation possible.

Tangled Pages. Origin-per-identity, path-per-site, backed by git repos and sh.tangled.repo records. The closest existing implementation of the addressing shape, differing in that it uses real DNS subdomains.

15. Open questions

  1. NSID. agency.portable.route is a placeholder. If this is meant to be a shared vocabulary, the domain it sits under is a governance question, not just a naming one, see how site.standard was established collaboratively rather than under one publisher.
  2. Remote non-executable subresources default: deny, or allow with policy? Deny is safer and breaks most existing sites.
  3. Multiple identities, same content. Two DIDs naming the same root CID get different origins. Correct, but confusing when it is the same publisher migrating.
  4. Rendering fidelity for inclusion-proven handlers. Rendering from site.standard.document records is a reader view, not the site, and the ecosystem's content-type parsing is still immature. Should the spec say so, or is that a client's problem?
  5. standard.site verification for route-only sites. The <link rel> handshake needs a reachable HTTPS document, which content at a derived origin does not have (Section 14.4). Accept the missing badge, define an atproto-native alternative, or treat a gateway URL as the answer?
  6. Reader privacy (Section 13). Is caching-for-privacy worth specifying as a recommended default, given it reintroduces an intermediary the rest of the design works to remove?
  7. Limit values (Section 8) are first guesses. All of them want validation against real content before they are treated as anything else.
  8. Blob reclamation tuning (Section 9.1). Staging records are correct regardless of reclamation policy, per Section 1, but batch sizing is not. Too large and an eager PDS reclaims mid-publish; too small and every publish costs needless commits. This wants measurement across implementations, not a single answer.
  9. A human title on route records. A name picker (Section 6.2) shows short names with no description. A title field would help, and would be publisher-controlled text displayed beside a name, the attack Section 5.9.3 covers for link text. Add it with the same never-treated-as-a-name rule, or leave names to speak for themselves?
  10. Staging collection NSID. Staging records need a collection no handler resolves, which means reserving one ecosystem-wide rather than per publisher, so clients can ignore it by name.

16. Scope: public content

This draft covers public websites. Both proof mechanisms in Section 4.3 prove against a public signed commit, so provable content is by construction publicly readable.

That is a scope decision, not a gap. Three things get called private and only one of them is deferred:

Unlisted. Content is public and fully provable; the name is unguessable, a random rkey rather than blog. Drafts, review links, pages the publisher would rather not advertise. No spec change; this is a publishing-UI affordance. Named here because it is what most requests for "private" actually want.

Access-controlled. Content behind an auth wall on some host, wisp's private sites (Section 14.1), or any gated HTTPS endpoint. Already handled: these are asserted targets, so they redirect (Section 4.3). The client cannot verify them, so they get no derived origin and make no proof claim. The cost is real and the treatment is accurate. Note that the disposition is fixed for everyone, per Section 4.3, an auth-gated target does not become provable for viewers who hold credentials.

Cryptographically private. Content whose bytes are on the PDS but unreadable. Deferred to atproto's own private-data work rather than invented here. When it lands, it enters as a third proof mechanism in Section 4.3 and touches nothing else: resolution, origins, rollback, and capability policy are all orthogonal to whether bytes are readable.

Three properties to carry into that work rather than rediscover:

A. Comparison with the web platform

This section is non-normative.

Same job, different answers. Section A.2 covers what HTTP needs that this design does not.

HTTPS + DNS + CA This spec
Name Domain, rented from a registrar @handle/name, a record in a repo
Link syntax https://host/path atw://handle/name/path (Section 3.1.2)
Cost of a name Per domain, annually, forever A record write. Names within an identity are free
Who authorizes it Registry → registrar → registrant Whoever controls the DID
Name → location DNS, cached by TTL Handle → DID → PDS → record, cached by rev (Section 3.4)
Authenticates The connection The bytes
Trust anchor ~150 root CAs, any of which can issue for any name The DID document, controlled by the publisher
Revocation CRL, OCSP, short-lived certs Rotation (Section 5.2); labelers as an advisory channel
Key rotation Reissue, name unaffected Rotate, name unaffected; and it clears the rollback floor
Origin scheme + host + port Derived from (DID, name) (Section 5.1)
Origin isolation cost A DNS name, and a certificate Nothing
Content integrity None beyond the transport; SRI per tag, opt-in Structural: every byte proven (Section 4.3)
Freshness ETag, Last-Modified, Cache-Control, expiry Monotonic rev + content CID (Section 5.2). No clocks anywhere
Mirrors Must be trusted; CDN sees and can alter plaintext Cannot lie, only withhold (Section 7)
Rollback protection None Required (Section 5.2)
Publisher key loss Reissue from the CA Rotate, if rotation keys were kept (Section 9.8)
Domain loss Total: name, content, and origin storage Handle loss costs the label; DID retains origins and storage
Readership Server logs, plus the CDN's Same, and worse when fetching direct (Section 13)
Offline Works only from cache, unverifiable Verifies fully offline against a cached CID
Reach Every browser Conforming clients; gateways for the rest (Section 7)

A.1 Naming layers compared

This section is non-normative.

System Key layer Nickname layer Petname layer
Web + DNS none Domain Bookmarks, degenerate
SSH Host key none known_hosts plus Host aliases
Secure Scuttlebutt Feed ID Self-assigned Yes
Tor onion services Address is the key none Bookmarks
Nostr npub NIP-05 identifier none
pkarr Public key none Explicitly deferred to callers
This spec DID Handle Optional, client-side (Section 5.11)

SSH is the most widely deployed working petname system and the closest structural analogue: a key the user never reads, a local alias they chose, and a loud warning when the binding changes. The web is the outlier in having no key layer at all, a domain is simultaneously the only identifier and the only name, which is why losing it loses everything (Section 13.3).

On Zooko's triangle, this design currently spends both its choices on global: the DID is secure and global but unmemorable, the handle is memorable and global but not secure. The memorable-and-secure corner is the one left unoccupied, and Section 5.11 is how a client occupies it without the protocol changing.

A.2 Mechanisms not needed

This section is non-normative.

Much of HTTP's accumulated machinery patches a gap this design closes lower down. Porting it adds surface without adding safety.

HTTP mechanism Why it is unnecessary here
ETag, Last-Modified, 304, Cache-Control A CID is a permanently valid validator. Only the name→CID pointer expires, under its own rules (Section 3.4)
Fingerprinted filenames Content addressing already makes aggressive caching safe. Fingerprinting defeats cross-version dedup by giving identical bytes different paths (Section 9.9)
Subresource Integrity Executables resolve from the manifest (Section 5.3), so integrity is structural rather than per-tag
CORS, preflight Blob fetches carry no ambient authority and mutate nothing. Section 5.3 is stricter and enforced at load time
HSTS, upgrade-insecure-requests, mixed-content blocking All defend a transport-derived guarantee this design does not rely on
DNSSEC, CAA, Certificate Transparency, pinning A DID plus a signed commit answers name ownership directly. DNS remains only for handle→DID, secured by Section 3.2's bidirectional check
rel=canonical, www-vs-apex, 301 migration One name resolves to one record. A handle change leaves the DID and every origin intact
robots.txt, sitemap.xml Records reach indexers via the firehose regardless, and listRecords enumerates a site more completely. robots.txt implies a withholding power the publisher does not have
Expiry, max-age, certificate validity windows No clocks anywhere in the proof chain. Nothing breaks on a wrong device clock and nothing expires unattended

Not everything is deletable. Content types (Section 5.5), path resolution (Section 5.6), error semantics (Section 5.7), and referrer policy (Section 5.8) are real problems the web solved for real reasons; Section 5 adopts the lessons with tighter defaults.

A.3 Web platform advantages

This section is non-normative.

Three areas.

Reach. Every device made in the last thirty years speaks HTTPS. This design reaches conforming clients, and everyone else through a gateway that reintroduces the trust properties it was built to remove. That is the central adoption cost, and Section 7 exists to manage it rather than deny it.

Ambient infrastructure. Caching, compression negotiation, range requests, load balancing, DDoS absorption, decades of accumulated operational tooling that assumes HTTP. Section 11.1 is largely a list of things a PDS will not do for you that nginx would.

Failure familiarity. When HTTPS breaks, operators know why. When this breaks, a floor rejection, a missing handler, an unsettled rotation, nobody has intuition yet. Section 5.7's requirements on error surfaces are a partial answer and not a substitute for operational experience.

A.4 Advantages of this design

This section is non-normative.

Names are not rented. The recurring cost of a domain is why most people publish inside someone else's namespace. A name here costs a record write, and names within an identity are free, which is the whole premise.

The trust anchor is the publisher. HTTPS asks you to trust that none of ~150 root CAs will issue for a name they do not control. Here the DID document is the anchor and the publisher controls it.

Integrity is structural, not transport-derived. HTTPS authenticates a connection; anything cached, mirrored, or replayed is unverifiable afterwards. Content proven against a signed commit stays proven, in a cache, on a mirror, on a laptop with no network.

Origin isolation is free. On the web, a separate origin costs a DNS name and a certificate, which is why so much unrelated content shares one. Here it costs a hash (Section 5.1).

Rollback protection exists at all. The web has no mechanism to stop a cache or mirror serving a superseded version.

No clocks. Nothing expires, so nothing breaks because a device's clock is wrong (Section A.2).

A.5 Summary

This section is non-normative.

This design trades reach for guarantees. The web's model reaches everyone with a weaker claim; this one reaches fewer people with a stronger one, the same axis Section 14.1 places wisp on, further along it.

That trade only pays if goal 1 holds. A design that reached fewer people and let names overstate themselves would be strictly worse than the web, which is why the constraints that look severe (no fallback, no synthesized origin for unverified content, no stale display) are the ones that cannot be relaxed for adoption's sake.

B. Content addressing, signing, and where the boundary goes

This section is non-normative.

Section 4.3 admits two proof mechanisms. This appendix is why, and what each costs.

B.1 Constraints on pure forms

This section is non-normative.

Pure content addressing cannot name anything. A CID identifies bytes; it says nothing about who published them or whether they are current. Any content-addressed system that wants a human-readable name needs a signed mutable pointer at the root. IPNS for IPFS, a signed record here. Pure CAS is not an option, only a choice about how deep it goes below the pointer.

Pure signing cannot carry bulk. atproto's own blob references are CIDs, so a repo holding anything larger than record-sized content is already a hybrid. Pure signing is not an option either.

The real question is therefore not which, but where the boundary sits between the signed layer that establishes what is current, and the content-addressed layer that gets bytes from anywhere.

B.2 Layer responsibilities

This section is non-normative.

Signed layer Content-addressed layer
Establishes Whose, and which version That these are the bytes
Wants to be Small, mutable, authoritative Large, immutable, cacheable
Needs at load Key material, DID resolution Nothing but a hash function
Rollback Native, via rev None; a CID is a fact, not a version
Dedup None Across versions, sites, identities
Mirrors Must be trusted or re-verified Trustless by construction

B.3 Boundary at the name (deep CAS)

This section is non-normative.

Signed record → root CID → whole tree content-addressed. wisp and IPFS+IPNS are examples.

Gains: atomic deploys, the root CID flips and the entire site changes at once, with no torn intermediate state. One signature verified per load regardless of resource count. Maximum dedup, since unchanged subtrees share CIDs across versions.

Costs: content is opaque to the firehose, indexers see blobs, not prose, so syndication and labeling have nothing to read. No per-resource granularity for moderation. A manifest is required, and it is an extra artifact to fetch and validate.

B.4 Boundary at each resource (shallow signing)

This section is non-normative.

Every resource is its own signed record, proven by MST inclusion. No root CID. standard.site, Leaflet.

Gains: content is legible on the firehose, so indexing, labeling, and syndication work with no extra publishing step. Per-resource granularity, delete one post. The collection is the site; no manifest to maintain.

Costs: no dedup; the same image in two sites is two blobs. N resources need N fetches and N inclusion proofs. Unsuited to binary assets of any size.

And a real hazard: torn deploys. Updating three documents as three commits lets a reader observe a mixed state, new index, old article. Two mitigations, and the spec should recommend both:

A pinned rev gives shallow signing exactly the snapshot consistency a root CID gives deep CAS. This is the closest thing to a free result in the comparison, and it is why Section 5.2's rev tracking earns its keep twice over.

But applyWrites has a ceiling, so the mitigation runs out exactly when a site gets large. Past that point atomicity comes only from Section 4.2's switching record: content accumulates across many commits and a generation pointer names which set is current. Deep CAS gets this for free, one manifest record switches a site of any size, while shallow signing has to construct it. Section 9.4 covers the mechanics.

This is a larger advantage for deep CAS than the rest of this appendix implies, and it grows with site size.

B.5 The hybrid model

This section is non-normative.

This spec lets the boundary move per target, unified by Section 4.3's disposition model. A publisher picks per site: prose takes shallow signing and gets indexed; an application takes deep CAS and gets atomic, dedup'd, binary-friendly delivery. One origin model covers both.

The costs:

Two proof paths to implement and test, with two failure modes. Section 12's vectors must cover both.

Revocation is asymmetric. Deleting a record removes it from the current MST, so it stops proving against current HEAD. Deleting a blob does not retract the CID from a manifest, and mirrors retain bytes. Deep-CAS content is therefore harder to unpublish than shallow-signed content. Note that neither is true revocation, anyone holding an older commit holds a proof that was valid when made (Section 13).

Enumeration is asymmetric. Shallow-signed sites are enumerable via listRecords; deep-CAS sites are opaque without the manifest. This matters for moderation tooling, which can survey one and not the other.

Clients implementing one mechanism reach less content. Such a client will encounter names it cannot load, and Section 4.4 forbids it from falling through to a weaker target instead.

B.6 Redundant mechanisms

This section is non-normative.

Signing blobs is redundant. A CID inside a proven record already transitively signs the bytes. Adding a separate signature over content-addressed content adds key handling for no gain.

Hashing record content is redundant. MST inclusion under a signed commit already proves it. This is why Section 4.3 keeps inclusion and content-address as distinct mechanisms rather than forcing everything through a hash.

B.7 Recommendation

This section is non-normative.

Keep the hybrid, and put manifests in records rather than blobs. A manifest as a record is MST-proven, appears on the firehose, and is therefore legible and labelable even when the content it points at is not, which recovers part of what deep CAS gives up, at negligible cost.

C. Worked example

This section is non-normative.

End to end, @portable.agency/blog entered or activated in a client, on a client implementing content-address proof and a content-addressed handler (Section 10.2).

1. Parse. Input form. Identity portable.agency, short name blog. Not a root name, so no self substitution (Section 4.7).

2. Handle → DID. DoH query for _atproto.portable.agency TXT returns did=did:plc:vypurakoz7ftwpsspcrjqdy7. Cached under the record's TTL, keyed on the DID (Section 3.4).

3. DID → document. Resolve the DID. Confirm alsoKnownAs contains at://portable.agency, without this the handle means nothing (Section 3.2). Read serviceEndpoint for the PDS. Note the verification method; the rev floor will be recorded against it (Section 5.2).

4. Account state. PDS reports active. Proceed (Section 3.2).

5. Route record.

Returns a CAR. Verify the commit signature against the DID document's verification method, verify MST inclusion, confirm the proven path's rkey is blog (Section 3.3). Record rev.

6. Rollback check. rev is at or above the stored floor for this origin, and the verification method is unchanged. Proceed (Section 5.2).

7. Validate the record. targets present, alias absent, 3 targets (within 16), no unknown keys inside policy (Section 4.1, Section 8).

8. Select. First target is at://…/example.handler.site/blog. The NSID maps to a handler this client has; disposition is load, proof by content address (Section 4.2, Section 4.3). Selection stops here, later targets are not attempted, and will not be attempted if this one fails (Section 4.4).

9. Derive the origin.

Ephemeral by default: session-only storage, no permissions (Section 5.4).

10. Load. Handler resolves the site record to a deployment to a MASL manifest. Manifest is itself a record, so it is MST-proven. Path / resolves per the handler's rules (Section 5.6) to index.html. Fetch the blob, re-hash against the CID, mismatch would be fatal (Section 10). Serve with the manifest's declared type; no sniffing (Section 5.5). Decompression bounded by ratio and ceiling (Section 8).

11. Subresources. style.css and app.js resolve from the manifest. A <script src> pointing off-origin would be rejected at load time, not at fetch time (Section 5.3).

12. Links. <a href="atw://other.example/notes">Notes</a> remains unresolved when the page renders and carries no verification claim. On activation or a deliberate user gesture such as hover-to-preview, the client resolves the name in full, including the bidirectional handle check. It presents the result according to Section 5.9.3; unresolvable links render inert (Section 5.9).

13. Display. Address bar shows @portable.agency/blog, re-resolved from the DID rather than cached (Section 5.10), with the integrity disposition reported (Section 4.3).

C.1 The same address, different record

This section is non-normative.

If the first target were https://blog.example.com/ instead, steps 9–13 would not happen. Disposition is redirect: the client navigates to that origin under ordinary web rules, the address bar shows blog.example.com, no origin is derived, no proof is claimed, and the referrer is suppressed on the way out (Section 4.3, Section 5.8).

Same name, same identity, same record type, a different disposition, and the user can see which one they got.

D. References

D.1 Normative references

[RFC2119]
Key words for use in RFCs to Indicate Requirement Levels. S. Bradner. IETF. March 1997. BCP 14. URL: https://www.rfc-editor.org/rfc/rfc2119
[RFC8174]
Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words. B. Leiba. IETF. May 2017. BCP 14. URL: https://www.rfc-editor.org/rfc/rfc8174