zds#
an at protocol personal data server.
name credit: jim (
calabro.io) suggestedzds.
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:
zatis 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 intozat.httpzis 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 forhttpzitself.zatandhttpzboth rely on Karl Seguin'swebsocket.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,
createAccountrejects 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.
/securityis the local account-security page for passkey and app-password management.