atproto pds in zig
zds docs development.md
5.3 kB
Markdown
at main

development #

This note records codebase conventions that are easy to lose while moving quickly.

source layout #

  • src/main.zig is process setup. Keep parsing and setup helpers in core or internal so main stays readable.
  • src/core is small shared PDS infrastructure: config, logging, syntax, XRPC helpers, mail, and repo helpers.
  • src/http owns routing and HTTP response shape. Endpoint behavior belongs in the protocol modules rather than in the router.
  • src/atproto owns the AT Protocol XRPC surface: server, repo, sync, OAuth, identity, preferences, and generic proxying.
  • src/storage owns SQLite persistence, repo mutation, event sequencing, and blob metadata. Runtime code reads record bodies from repo_blocks, not from cached JSON columns.
  • src/internal is ZDS-local reusable glue. Good candidates are CLI parsing, sharded synchronization, passkey adapters, and narrow encoding adapters. Protocol primitives still belong in zat once they are general enough.
  • bench and tools contain repeatable local probes. Prefer Just targets over one-off command lines.

dependency boundary #

Treat ZDS as an application and zat as the sibling library we maintain for reusable AT Protocol code. ZDS should depend on zat; it should not copy, vendor, or fork zat to make local package graph problems disappear.

  • Use zat for atproto primitives: syntax, TIDs, DID and handle resolution, JWT helpers, DAG-CBOR, CAR, MST, repo verification, OAuth helpers, JSON helpers, and streaming clients.
  • Use httpz for the PDS HTTP/1.1 server boundary: accept loops, request parsing, response writing, upgrades, and server-side websocket integration.
  • Use webauthn for browser credential ceremony verification. ZDS owns the account policy, challenge storage, and UI that surround it.
  • Use zqlite for SQLite access. ZDS owns the schema and transaction policy.

When ZDS needs a generally useful primitive that zat does not expose yet, build the smallest local version under src/internal, use it from ZDS, and document why it is likely upstream material. Once the shape is clear, upstream it into zat and repin ZDS to the new commit or release.

When dependency packages disagree, fix the package that owns the disagreement. Because we maintain zat, dependency alignment problems in zat should be fixed in the zat repo and consumed here by pinning the resulting commit. Do not vendor zat into ZDS. Do not vendor httpz either; consume it directly from Tangled and send focused fixes upstream when its HTTP or websocket behavior needs to change.

Keep the websocket graph singular. httpz and zat should both use the canonical first-party zzstoatzz.io/websocket.zig package so ZDS does not pull two websocket implementations into one build.

Vendoring is only acceptable as a temporary local debugging move. Before a change is considered ready, remove vendor/, express dependencies in build.zig.zon, and verify the project from a clean package graph.

For generated JSON, prefer typed structs plus std.json.Stringify.valueAlloc or a narrow helper over hand-written object strings. std.json.fmt is still useful at stream boundaries, but new response code should avoid assembling JSON objects with format strings when a structured value is straightforward.

lessons from history #

  • Browser behavior wins over assumptions. If a client reports a failure, check the actual request, response, and browser console before deploying a fix.
  • Appview behavior belongs behind the generic atproto-proxy path. Do not add local Bluesky appview route implementations.
  • Unknown record schemas may be accepted, but must not be reported as validationStatus: "valid" without validation.
  • OAuth client assertions are JOSE signatures. Accept standard raw ECDSA JWS signatures for OAuth without weakening stricter repo-signature verification.
  • Passkey support should stay close to Tranquil's WebAuthn model: account-owned credential rows plus challenge rows, with browser prompts opened directly from user gestures.
  • Account-plane security state should stay durable. Password sessions are not just signed JWTs; storage tracks active access/refresh JTIs, refresh rotation, revocation state, auth method, and optional future controller DID.
  • When adding account-management behavior, record subject DID, actor DID, and optional controller DID separately. That keeps the path toward Tranquil-style delegated accounts explicit instead of embedding policy in ad hoc token strings.
  • Storage shape follows the official PDS SQLite model where practical: record index rows identify current CIDs, and canonical record content is stored as DAG-CBOR blocks.
  • Benchmark tables should only compare equivalent units of work. Put adjacent probes in a separate section rather than mixing them into one matrix.

local state #

Local SQLite files under dev/ are ignored workspace state. Do not commit database files, WAL files, .env, .zig-cache, zig-out, or package cache directories.

commands #

The root justfile is the stable task surface:

just test
just smoke
just invite https://pds.zat.dev
just plc-repair did:plc:...
just bench all
just docker-publish-current

Run zig zen before committing.