···11# zds
2233-A Zig experiment toward an AT Protocol Personal Data Server.
44-55-> Name credit: Jim (`calabro.io`) suggested `zds`.
33+a zig implementation of an at protocol personal data server.
6477-`zds` is a Zig implementation of an AT Protocol Personal Data Server. The goal
88-is a rigorous, production-correct PDS that follows the protocol and official PDS
99-behavior while keeping Bluesky-specific compatibility isolated from the core
1010-server:
55+> name credit: jim (`calabro.io`) suggested `zds`.
1161212-- real account, identity, repo, sync, and blob hosting
1313-- durable storage with explicit migration and recovery paths
1414-- production account migration support
1515-- compatibility with existing PDS implementations and clients
1616-- isolated Bluesky app-view compatibility where clients require it
77+`zds` stores atproto accounts, repos, records, blobs, sessions, and identity
88+state. it uses [`zat`](../zat.dev/zat) for atproto primitives such as syntax,
99+did resolution, jwt helpers, dag-cbor, car, mst, repo verification, and key
1010+encoding.
17111818-## Reference Projects
1212+## references
19132020-The initial design pass is informed by:
1414+the implementation is informed by:
21152216- `tranquil.farm/tranquil-pds`
2317- `futur.blue/pegasus`
···2620- `chadtmiller.com/pds.js`
2721- `zzstoatzz.io/pds-message-poc`
2822- `zzstoatzz.io/pollz`
2323+- `haileyok/cocoon`
29243030-See [docs/reference-study.md](docs/reference-study.md).
2525+see [docs/reference-study.md](docs/reference-study.md).
31263232-## Development
2727+## run locally
33283429```sh
3530zig build test
···3732caddy run --config dev/Caddyfile
3833```
39344040-Before committing, run the relevant probes plus:
3535+before committing:
41364237```sh
4338zig build test
4439zig zen
4540```
46414747-Then reflect on whether the change reduces what future readers must remember.
4242+## endpoints
48434949-The local server currently exposes:
4444+current server surface includes:
50455146- `GET /`
5247- `GET /.well-known/did.json`
···5853- `POST /xrpc/com.atproto.server.refreshSession`
5954- `GET /xrpc/com.atproto.server.getSession`
6055- `GET /xrpc/com.atproto.server.getServiceAuth`
6161-- migration-oriented repo, blob, account-status, email, and PLC submission endpoints
5656+- repo read/write/import/blob endpoints
5757+- sync repo/blob/status endpoints
5858+- identity plc credential/sign/submit endpoints
5959+- a small set of `app.bsky.*` compatibility endpoints
62606363-Blob upload requests default to a 100,000,000-byte ceiling for Bluesky video
6464-compatibility and can be changed with `ZDS_BLOB_UPLOAD_LIMIT` or
6565-`--blob-upload-limit`.
6161+blob uploads default to `100000000` bytes for bluesky video compatibility. set
6262+`ZDS_BLOB_UPLOAD_LIMIT` or `--blob-upload-limit` to change it.
66636767-See [docs/smoke.md](docs/smoke.md) for the first `atproto-smoke` run.
6464+see [docs/smoke.md](docs/smoke.md) for the first `atproto-smoke` run.
68656969-## Production Migration
6666+## deploy
70677171-For a throwaway migration target, run with a public HTTPS URL and mail config:
6868+run behind a public https origin:
72697370```sh
7471ZDS_RESEND_API_KEY=... \
···8380 --server-did did:web:pds.example.com
8481```
85828686-The migration path now includes service-auth-gated account creation for existing
8787-DIDs, real repo writes with per-account signing keys, verified repo CAR import,
8888-repo export, blob import checks, account activation state, recommended DID
8989-credentials, PLC operation signing/submission, and email delivery via Resend
9090-with local log fallback. Deployment also serves the PDS `did:web` document and
9191-handle `/.well-known/atproto-did` resolution.
8383+configuration:
92849393-The next production risks are operational rather than fake core shortcuts:
9494-sequenced subscription events, durable session/recovery flows, and a filesystem
9595-or object-store blob backend with backup/restore tooling. See
9696-[docs/production-migration.md](docs/production-migration.md).
8585+- `--public-url`: public pds origin used in did credentials and blob urls
8686+- `--server-did`: pds service did, usually `did:web:<host>`
8787+- `ZDS_HANDLE_DOMAINS`: comma-separated handle domains advertised by
8888+ `describeServer`
8989+- `ZDS_JWT_SECRET`: stable secret for access and refresh jwt signing
9090+- `ZDS_RESEND_API_KEY` and `ZDS_EMAIL_FROM`: email delivery for account and plc
9191+ tokens
9292+- `ZDS_BLOB_UPLOAD_LIMIT`: generic `com.atproto.repo.uploadBlob` body limit
9393+9494+## account migration
9595+9696+zds implements the pds endpoints used by browser-based migration tools such as
9797+[pds moover](https://pdsmoover.com/):
9898+9999+- service-auth-gated `createAccount` for an existing did
100100+- repo car import/export
101101+- missing blob reporting and blob upload
102102+- preferences import/export
103103+- recommended did credentials
104104+- plc operation email token, signing, and submission
105105+- account activation/deactivation and status checks
106106+107107+see [docs/migration.md](docs/migration.md).
108108+109109+## storage
110110+111111+sqlite stores accounts, records, repo blocks, commits, expected blobs, imported
112112+blobs, preferences, bookmarks, and mute state. uploaded blobs are currently
113113+stored in sqlite as blob rows.
114114+115115+## next
116116+117117+- `com.atproto.sync.subscribeRepos`
118118+- password reset and account recovery flows
119119+- filesystem or object-store blob storage
120120+- backup/restore docs for the sqlite database and blob store
+95
docs/migration.md
···11+# migration
22+33+`zds` implements the pds endpoints used by browser-based account migration
44+tools, including pds moover.
55+66+## pds moover flow
77+88+pds moover asks for:
99+1010+- current handle and password
1111+- new pds url
1212+- new email
1313+- new handle
1414+- invite code
1515+1616+the migration is not complete until the user enters a plc token delivered by
1717+email.
1818+1919+## implemented endpoints
2020+2121+implemented:
2222+2323+- `com.atproto.server.createAccount`
2424+- `com.atproto.server.checkAccountStatus`
2525+- `com.atproto.server.activateAccount`
2626+- `com.atproto.server.deactivateAccount`
2727+- `com.atproto.server.refreshSession`
2828+- `com.atproto.repo.importRepo`
2929+- `com.atproto.repo.listMissingBlobs`
3030+- `com.atproto.sync.getRepo`
3131+- `com.atproto.sync.listBlobs`
3232+- `com.atproto.sync.getRepoStatus`
3333+- `com.atproto.identity.getRecommendedDidCredentials`
3434+- `com.atproto.identity.requestPlcOperationSignature`
3535+- `com.atproto.identity.signPlcOperation`
3636+- `com.atproto.identity.submitPlcOperation`
3737+- `app.bsky.actor.getPreferences`
3838+- `app.bsky.actor.putPreferences`
3939+4040+implementation details:
4141+4242+- `createAccount` verifies migration service jwts against the migrating did
4343+- session tokens are signed with the configured `ZDS_JWT_SECRET`
4444+- stored passwords use salted pbkdf2-sha256 hashes
4545+- repo writes produce signed commit blocks and mst updates
4646+- imported repo cars are verified against the account did and signing key
4747+- each account has its own signing key for repo commits and plc credentials
4848+- `uploadBlob` defaults to 100,000,000 bytes and is configurable with
4949+ `ZDS_BLOB_UPLOAD_LIMIT` or `--blob-upload-limit`
5050+- the server serves `/.well-known/did.json` for the pds service did and
5151+ `/.well-known/atproto-did` for hosted handles
5252+5353+## not yet implemented
5454+5555+- `com.atproto.sync.subscribeRepos` and real sequenced account/identity events
5656+- password reset and account recovery paths
5757+- filesystem or object-store blob backend with backup/restore tooling
5858+5959+## deployment
6060+6161+run behind a public https origin:
6262+6363+```sh
6464+ZDS_RESEND_API_KEY=... \
6565+ZDS_EMAIL_FROM='ZDS <pds@example.com>' \
6666+ZDS_BLOB_UPLOAD_LIMIT=100000000 \
6767+ZDS_HANDLE_DOMAINS='.example.com,example.com' \
6868+ZDS_JWT_SECRET='at-least-32-random-bytes-here' \
6969+zig build run -- \
7070+ --port 2583 \
7171+ --db /var/lib/zds/zds.sqlite3 \
7272+ --public-url https://pds.example.com \
7373+ --server-did did:web:pds.example.com
7474+```
7575+7676+the public url is used for did credentials and blob views. `ZDS_RESEND_API_KEY`
7777+and `ZDS_EMAIL_FROM` enable real email delivery; without them, token emails are
7878+logged to stdout for local development. `ZDS_BLOB_UPLOAD_LIMIT` controls the
7979+generic `com.atproto.repo.uploadBlob` request body ceiling.
8080+`ZDS_HANDLE_DOMAINS` is what `describeServer` advertises to clients.
8181+`ZDS_JWT_SECRET` signs app-session access and refresh jwts; keep it stable
8282+across deploys or existing sessions will be invalidated.
8383+8484+the at protocol uploadBlob lexicon does not define one universal byte limit; it
8585+says blob restrictions are enforced when a record references the uploaded blob.
8686+the official pds exposes a configurable blob upload limit, while the bluesky
8787+video embed lexicon allows 100,000,000-byte mp4 blobs. `zds` uses that as the
8888+default generic upload ceiling so migration tests do not fail below bluesky's
8989+current video shape.
9090+9191+deployment also needs sqlite data-directory backups and operational access to
9292+inspect account status, repo import, missing blobs, and plc submission failures.
9393+9494+core atproto account, repo, sync, and identity code should stay separate from
9595+bluesky app-view compatibility. bluesky compatibility lives in `bsky_*` modules.
-99
docs/production-migration.md
···11-# Production Migration
22-33-`zds` now has the migration surface needed to try a low-stakes throwaway account
44-migration. Passing the local smoke suite was only the baseline; the important
55-surface here is account creation under service auth, repo import/export, blob
66-transfer, PLC handoff, and email-token delivery.
77-88-## PDS MOOver Shape
99-1010-PDS MOOver asks for:
1111-1212-- current handle and password
1313-- new PDS URL
1414-- new email
1515-- new handle
1616-- invite code
1717-1818-The migration is not complete until the user enters a PLC token delivered by
1919-email. That means `zds` needs both migration endpoints and outbound email before
2020-it can be used safely with that flow.
2121-2222-## Implemented Migration Surface
2323-2424-Implemented:
2525-2626-- `com.atproto.server.createAccount`
2727-- `com.atproto.server.checkAccountStatus`
2828-- `com.atproto.server.activateAccount`
2929-- `com.atproto.server.deactivateAccount`
3030-- `com.atproto.server.refreshSession`
3131-- `com.atproto.repo.importRepo`
3232-- `com.atproto.repo.listMissingBlobs`
3333-- `com.atproto.sync.getRepo`
3434-- `com.atproto.sync.listBlobs`
3535-- `com.atproto.sync.getRepoStatus`
3636-- `com.atproto.identity.getRecommendedDidCredentials`
3737-- `com.atproto.identity.requestPlcOperationSignature`
3838-- `com.atproto.identity.signPlcOperation`
3939-- `com.atproto.identity.submitPlcOperation`
4040-- `app.bsky.actor.getPreferences`
4141-- `app.bsky.actor.putPreferences`
4242-4343-The migration-critical implementation details are also in place:
4444-4545-- `createAccount` verifies migration service JWTs against the migrating DID
4646-- session tokens are signed with the configured `ZDS_JWT_SECRET`
4747-- stored passwords use salted PBKDF2-SHA256 hashes
4848-- repo writes produce signed commit blocks and MST updates, not synthetic refs
4949-- imported repo CARs are verified against the account DID and signing key
5050-- each account has its own signing key for repo commits and PLC credentials
5151-- `uploadBlob` defaults to 100,000,000 bytes and is configurable with
5252- `ZDS_BLOB_UPLOAD_LIMIT` or `--blob-upload-limit`
5353-- the server serves `/.well-known/did.json` for the PDS service DID and
5454- `/.well-known/atproto-did` for hosted handles
5555-5656-Still to harden after a throwaway migration:
5757-5858-- `com.atproto.sync.subscribeRepos` and real sequenced account/identity events
5959-- password reset and account recovery paths
6060-- filesystem or object-store blob backend with backup/restore tooling
6161-6262-## Deployment Requirements
6363-6464-Run behind a public HTTPS origin:
6565-6666-```sh
6767-ZDS_RESEND_API_KEY=... \
6868-ZDS_EMAIL_FROM='ZDS <pds@example.com>' \
6969-ZDS_BLOB_UPLOAD_LIMIT=100000000 \
7070-ZDS_HANDLE_DOMAINS='.example.com,example.com' \
7171-ZDS_JWT_SECRET='at-least-32-random-bytes-here' \
7272-zig build run -- \
7373- --port 2583 \
7474- --db /var/lib/zds/zds.sqlite3 \
7575- --public-url https://pds.example.com \
7676- --server-did did:web:pds.example.com
7777-```
7878-7979-The public URL is used for DID credentials and blob views. `ZDS_RESEND_API_KEY`
8080-and `ZDS_EMAIL_FROM` enable real email delivery; without them, token emails are
8181-logged to stdout for local development. `ZDS_BLOB_UPLOAD_LIMIT` controls the
8282-generic `com.atproto.repo.uploadBlob` request body ceiling.
8383-`ZDS_HANDLE_DOMAINS` is what `describeServer` advertises to clients.
8484-`ZDS_JWT_SECRET` signs app-session access and refresh JWTs; keep it stable
8585-across deploys or existing sessions will be invalidated.
8686-8787-The AT Protocol uploadBlob lexicon does not define one universal byte limit; it
8888-says blob restrictions are enforced when a record references the uploaded blob.
8989-The official PDS exposes a configurable blob upload limit, while the Bluesky
9090-video embed lexicon allows 100,000,000-byte MP4 blobs. `zds` uses that as the
9191-default generic upload ceiling so migration tests do not fail below Bluesky's
9292-current video shape.
9393-9494-Deployment also needs SQLite data-directory backups and operational access to
9595-inspect account status, repo import, missing blobs, and PLC submission failures.
9696-9797-The implementation should keep following the current split: core atproto
9898-account, repo, sync, and identity code should not gain Bluesky app-view
9999-behavior; Bluesky compatibility stays in `bsky_*` modules.