The AT Protocol

The AT Protocol (ATProto) is a decentralized networking protocol for social applications. It provides portable identity, interoperation between services, and a shared data model so that content and relationships are not siloed per app. This page is the technical overview in the shape that Boris cares about: the pieces that let a Boris-built static site become an on-network publication.

For the Boris side, see Boris; for the loop that ties them together, see Standard.site — the Boris/ATProto loop.

The pieces Boris touches

ATProto has several layers; the ones that matter for a published site are:

DIDs

A DID is a self-certifying, portable identifier. ATProto uses did:plc: (the PLC method, the dominant production method) and did:web: (DNS-anchored). A DID resolves to a DID document that points at the account's PDS and authorization server. Portability means the owner can move the account to a different PDS while keeping the same identifier and records.

This site's profile (standard-site.json) is wired to the real identity did:plc:jiokpoojzqntdpyw5xvfr7rv (handle boris.filed.fyi), which resolves to the PDS at https://auriporia.us-west.host.bsky.network and the authorization server https://bsky.social.

PDS and the HTTP API

The Personal Data Server hosts the account's repository. It speaks an HTTP API (XRPC) for reading and writing records. A write to a PDS goes through:

  1. Discovery — resolve the DID to a PDS origin and authorization server metadata (RFC 9728).
  2. Authorization — obtain a credential that authorizes the write.
  3. The write — com.atproto.repo.createRecord / putRecord / deleteRecord against the PDS.
  4. Readback — fetch the record by its AT-URI to confirm it landed.

An AT-URI (at://<did>/<collection>/<rkey>) is the canonical address of a record. It is stable across moves because it is keyed by the DID, not the PDS host.

Records and lexicons

A record is a typed JSON value stored at collection + rkey under the owner's DID. The collection name namespaces the record's lexicon (e.g. site.standard.publication, site.standard.document). A lexicon defines the record's fields and validation rules. The standard.site family is small and deliberate:

This is the shape Boris projects and publishes. boris standard-site records dumps the canonical payloads offline (see the Standard.site — the Boris/ATProto loop guide).

Verification surface

Because a site.standard.publication record points a DID at a URL, publication is verifiable: a reader fetches the DID's record, learns the URL, and checks that the URL serves what the record claims. Boris's standard-site verify cross-checks the built tree against this projection offline — it confirms the head links and the .well-known/site.standard.publication file are consistent before any network write.

The two write paths

ATProto offers two ways to authorize a write, and Boris supports both. They are not interchangeable, and the choice matters operationally.

OAuth + DPoP (granular scopes)

The primary, scoped path. A client requests granular repository scopes (e.g. repo:site.standard.document, repo:site.standard.publication) via OAuth, and proves possession of the token with DPoP (RFC 9449, Demonstrating Proof of Possession). Each write is signed with a DPoP proof and a server-issued nonce.

Operational note (recorded 2026-08-27): against bsky.social / bsky.network PDS instances, the OAuth + DPoP write path fails at the DPoP nonce layer with InvalidNonce, reproducibly, on a freshly signed-in session. Read-side discovery and token exchange succeed; the record writes are rejected. See Test Runs for the full finding. This means OAuth writes currently need a self-hosted PDS that grants the granular collection scopes.

App-password (broad write, Bearer)

The opt-in fallback. An app-password is a long-lived, broad-write credential (a Bearer token) that works on bsky.social. Boris reads it once from stdin only — never from argv, profile, or environment.

Operational note (recorded 2026-08-27): the app-password path fully passed a live smoke against a throwaway test identity (did:plc:xrqjadveiamk7vq7sfnvfddz / pholiota.us-west.host.bsky.network): discovery, authorization, write (publication + document, unique rkeys), readback (AT-URI + value + CID verified for both records), and cleanup (both deleted, zero residue). See evidence/live-smoke.json and Test Runs. The production identity for this site is now did:plc:jiokpoojzqntdpyw5xvfr7rv (boris.filed.fyi, PDS auriporia.us-west.host.bsky.network).

Which path to use

OAuth + DPoP App-password
Scope granular (site.standard.*) broad account write
Token binding DPoP key binding Bearer
Works on bsky.social ✗ (writes fail with InvalidNonce) ✓
Credential entry browser OAuth stdin only
Least privilege yes no — use a disposable identity

The supported live parity target on bsky.social today is the app-password path. OAuth writes need a compliant PDS.

Standards referenced

The live smoke's spec.baseline records the authoritative surfaces the behavior was validated against:

The next page, Standard.site — the Boris/ATProto loop, shows how these pieces combine into the publish loop that turns a Boris build into an on-network site.