atproto pds in zig pds.zat.dev
pds atproto
24

Configure Feed

Select the types of activity you want to include in your feed.

210 3 4

Clone this repository

https://tangled.org/zat.dev/zds https://tangled.org/did:plc:6atzzmsifkfmfqmeqrl6jwzo
git@tangled.org:zat.dev/zds git@tangled.org:did:plc:6atzzmsifkfmfqmeqrl6jwzo

For self-hosted knots, clone URLs may differ based on your setup.



README.md

zds#

an at protocol personal data server.

name credit: jim (calabro.io) suggested zds.

zds stores atproto accounts, repos, records, blobs, sessions, oauth state, account-security state, and identity state. it uses zat for protocol primitives: syntax, tids, did resolution, jwt helpers, dag-cbor, car, mst, repo verification, and key encoding. WebAuthn support comes from webauthn.

dependency posture#

ZDS is an application. Dependencies are chosen around that boundary:

  • zat is the sibling library we maintain for reusable AT Protocol primitives. If ZDS grows a generally useful atproto parser, codec, verifier, resolver, or client helper, expect it to move upstream into zat.
  • httpz is the HTTP/1.1 server boundary. ZDS should consume it directly from Tangled, not vendor it. HTTP server behavior should stay local to ZDS unless it is a focused compatibility fix or improvement for httpz itself.
  • zat and httpz both rely on Karl Seguin's websocket.zig. Keep the graph on one websocket implementation; fix the upstream package we control rather than copying it into this repo.
  • Vendoring is a last resort for short-lived debugging only. Do not leave vendor/ as the dependency strategy.

See development for the longer decision rules.

docs#

run locally
zig build test
zig build run -- --port 2583 --db dev/zds.sqlite3
caddy run --config dev/Caddyfile

before committing:

just test
just smoke
zig zen
configuration

run behind a public https origin:

ZDS_RESEND_API_KEY=... \
ZDS_EMAIL_FROM='ZDS <pds@example.com>' \
ZDS_BLOB_UPLOAD_LIMIT=100000000 \
ZDS_BLOBSTORE_PATH=/var/lib/zds/blobs \
ZDS_HANDLE_DOMAINS='.example.com,example.com' \
ZDS_CRAWLERS='https://bsky.network,https://vsky.network' \
ZDS_PLC_ROTATION_KEY='64-hex-secp256k1-secret-or-private-multikey' \
ZDS_RECOVERY_DID_KEY='did:key:optionalRecoveryKey' \
ZDS_JWT_SECRET='at-least-32-random-bytes-here' \
ZDS_ADMIN_TOKEN='another-random-secret' \
ZDS_INVITE_REQUIRED=true \
zig build run -- \
  --port 2583 \
  --db /var/lib/zds/zds.sqlite3 \
  --public-url https://pds.example.com \
  --server-did did:web:pds.example.com

see operations for config notes.

container image

published to atcr.io:

docker pull atcr.io/zat.dev/zds:latest
docker run -p 2583:2583 \
  -v zds-data:/data \
  -e ZDS_PUBLIC_URL=https://pds.example.com \
  -e ZDS_SERVER_DID=did:web:pds.example.com \
  -e ZDS_JWT_SECRET='at-least-32-random-bytes-here' \
  atcr.io/zat.dev/zds:latest

to publish:

just docker-publish
just docker-publish v0.1.0
just docker-publish-current

behavior#

  • Appview XRPCs are reached through atproto-proxy.
  • Known record schemas are validated before writes are accepted.
  • Unknown record schemas are accepted with validationStatus: "unknown", matching the official PDS behavior when validation is not explicitly required.
  • Uploaded blob bytes are stored in the configured disk blobstore.
  • Invite codes use the official PDS table shape: code metadata plus recorded uses. When invites are required, createAccount rejects missing, disabled, or exhausted codes.
  • App passwords use durable account-owned rows and can be listed or revoked. Revocation also invalidates active sessions created with that app password.
  • Passkeys are optional account credentials for the OAuth login page. ZDS stores credential IDs, public keys, counters, names, and last-use timestamps.
  • /security is the local account-security page for passkey and app-password management.

references#