atproto pds in zig pds.zat.dev
pds atproto
zds docs architecture.md
6.9 kB

architecture #

zds is a Zig implementation of an AT Protocol Personal Data Server.

The server owns account state, repo state, sessions, OAuth authorization, service auth, blobs, sync output, and identity operations. It does not implement an appview. Appview requests are forwarded through the generic atproto-proxy path: to the service the proxy header names, or to the configured appview for app.bsky.* methods sent without one.

compatibility #

Start with the AT Protocol specifications and cross-check the reference PDS. Document intentional differences. Experimental Spaces APIs track the upstream alpha behind ZDS_PERMISSIONED_DATA and can change incompatibly.

Routing, /api, and /api/openapi.json use the same comptime endpoint inventory.

boundaries #

  • zat owns AT Protocol primitives: syntax, TIDs, DID/handle resolution, JWT helpers, DAG-CBOR, CAR, MST, repo verification, OAuth helpers, and streaming clients.
  • httpz owns the HTTP/1.1 server mechanics: request parsing, response serialization, connection handling, upgrades, and server-side websocket plumbing. ZDS adapts AT Protocol handlers onto that server boundary.
  • zds owns PDS policy and persistence: accounts, repo writes, sessions, app passwords, passkeys, storage, server routes, sync production, account migration, and the local login/security surface.
  • Dependency fixes should happen at the owning layer. Since this project also maintains zat, ZDS should consume a fixed zat commit rather than vendoring or patching a private copy.
  • Spaces HTTP-signature parsing and profile validation stay in src/internal/space_signature.zig; zat supplies DID-key decoding and JOSE verification. This is a narrow Spaces profile, not a general HTTP-signature API.
  • SQLite stores account, repo, commit, token, OAuth, blob metadata, and identity state, plus Spaces revocations, authority sequencing, and pending notifications.
  • Password sessions are durable rows keyed by access and refresh JWT IDs. Refresh rotates the stored token family instead of trusting self-contained JWTs alone.
  • Account-plane audit events store subject, actor, and optional controller DID fields. That mirrors Tranquil's delegation-ready model without exposing user delegation before the surrounding account controls exist.
  • Invite codes follow the official PDS split between code metadata and recorded uses; account creation consumes a code while holding the store lock.
  • Blob bytes live in the disk blobstore rooted at ZDS_BLOBSTORE_PATH.
  • Passkey storage follows Tranquil's core shape: credential ID, public key, signature counter, friendly name, and timestamps. Passkeys are outside the official PDS surface ZDS tracks for compatibility.

repo write invariants #

Repo writes must match reference PDS behavior:

  • $type must match the collection being written.
  • omitted rkeys are generated as TIDs.
  • known key: "tid" records reject explicit non-TID rkeys.
  • known literal:self records require rkey self.
  • known records return validationStatus: "valid".
  • unknown lexicons return validationStatus: "unknown" unless a known schema is available and validation succeeds.
  • writes update the MST, write a signed commit block, emit sync events, and request relay crawls.

These rules are grounded in bluesky-social/atproto/packages/pds record preparation and Tranquil's validation/conformance model.

sync #

subscribeRepos and sync read endpoints expose the local repo state. Relay visibility depends on valid repo commits, coherent event frames, and crawl requests. ZDS_CRAWLERS should include both https://bsky.network and https://vsky.network for current deployment coverage.

auth and login #

OAuth follows the ATProto OAuth profile: PAR is required, redirect URIs are validated against client metadata, permission-set includes must resolve, tokens enforce granular repo/blob/rpc/account/identity/space scopes, DPoP-bound OAuth access tokens must present matching proof headers on resource requests, and revocation affects resource-server checks. Redirect matching is exact, with the RFC 8252 loopback exception the reference provider applies: a client whose metadata says application_type: native and registers http://127.0.0.1/callback (or the [::1] form) without a port matches that redirect on whatever port it bound at request time. A registered port still has to match and localhost is not a loopback host for this redirect exception. Separately, the virtual OAuth client ID http://localhost supports the ATProto localhost development profile, with loopback-IP redirect URIs and metadata supplied in its query string.

Password sessions and app passwords match the reference PDS account model. Session JWTs are accepted only when their JTI is present in the active session table, so revocation and refresh rotation can be enforced by storage. The answers a client gets for an expired, revoked, or replayed token are in account security. App-password sessions are attributed to the app-password name, and revoking the credential revokes those sessions too. The session schema already carries an optional controller_did column for future delegated-account flows, following Tranquil's controller-vs-subject split.

Passkeys are an additional OAuth login credential inspired by Tranquil, backed by the local webauthn dependency and stored in SQLite. Discoverable passkey login is supported so an account can be chosen by the authenticator during the OAuth flow.

/account is the resident management surface. Its security section, /account/manage, manages passkeys and app passwords with account-password authentication, matching the official PDS boundary that app passwords cannot mint or revoke other app passwords.

PDS-owned app.bsky.actor preferences are handled locally because the official PDS implements those routes. Appview reads and writes remain behind the generic atproto-proxy path.

account web surfaces #

Follow the reference PDS account manager's categories: Home, Account, Devices, Apps, About. ZDS currently exposes Home, Account, Apps, About. Credentials (passkeys and app passwords), email, and account status belong together under /account/manage; /account/security, /security, and /passkeys redirect there. App access uses /account/apps, with /account/sessions retained as an alias. Do not add a separate security/settings dashboard for each feature.

The experimental /account/spaces data viewer remains reachable from Home but is not an account-management navigation section. A future device-management view should use the reference Devices category instead of introducing another settings concept.

Reference: bluesky-social/atproto, packages/oauth/oauth-provider-ui/src/routes/account/u/$accountId/route.tsx (inspected 2026-09-05).