migration#
zds implements the pds endpoints used by browser-based account migration
tools, including pds moover.
pds moover flow#
pds moover asks for:
- current handle and password
- new pds url
- new email
- new handle
- invite code
the migration is not complete until the user enters a plc token delivered by email.
implemented endpoints#
implemented:
com.atproto.server.createAccountcom.atproto.server.checkAccountStatuscom.atproto.server.activateAccountcom.atproto.server.deactivateAccountcom.atproto.server.refreshSessioncom.atproto.repo.importRepocom.atproto.repo.listMissingBlobscom.atproto.sync.getRepocom.atproto.sync.listBlobscom.atproto.sync.getRepoStatuscom.atproto.identity.getRecommendedDidCredentialscom.atproto.identity.requestPlcOperationSignaturecom.atproto.identity.signPlcOperationcom.atproto.identity.submitPlcOperationapp.bsky.actor.getPreferencesapp.bsky.actor.putPreferences
implementation details:
createAccountverifies migration service jwts against the migrating did- session tokens are signed with the configured
ZDS_JWT_SECRET - stored passwords use salted pbkdf2-sha256 hashes
- repo writes produce signed commit blocks and mst updates
- imported repo cars are verified against the account did and signing key
- each account has its own signing key for repo commits and plc credentials
uploadBlobdefaults to 100,000,000 bytes and is configurable withZDS_BLOB_UPLOAD_LIMITor--blob-upload-limit- uploaded blob bytes are stored in the disk blobstore rooted at
ZDS_BLOBSTORE_PATHor--blobstore-path - the server serves
/.well-known/did.jsonfor the pds service did and/.well-known/atproto-didfor hosted handles
not yet implemented#
- real sequenced account/identity events
- password reset and account recovery paths
- object-store blob backend with backup/restore tooling
deployment#
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_JWT_SECRET='at-least-32-random-bytes-here' \
zig build run -- \
--port 2583 \
--db /var/lib/zds/zds.sqlite3 \
--public-url https://pds.example.com \
--server-did did:web:pds.example.com
the public url is used for did credentials and blob views. ZDS_RESEND_API_KEY
and ZDS_EMAIL_FROM enable real email delivery; without them, token emails are
logged to stdout for local development. ZDS_BLOB_UPLOAD_LIMIT controls the
generic com.atproto.repo.uploadBlob request body ceiling.
ZDS_BLOBSTORE_PATH controls where uploaded blob bytes are stored.
ZDS_HANDLE_DOMAINS is what describeServer advertises to clients.
ZDS_JWT_SECRET signs app-session access and refresh jwts; keep it stable
across deploys or existing sessions will be invalidated.
the at protocol uploadBlob lexicon does not define one universal byte limit; it
says blob restrictions are enforced when a record references the uploaded blob.
the official pds exposes a configurable blob upload limit, while the bluesky
video embed lexicon allows 100,000,000-byte mp4 blobs. zds uses that as the
default generic upload ceiling so migration tests do not fail below bluesky's
current video shape.
deployment also needs sqlite data-directory backups and operational access to inspect account status, repo import, missing blobs, and plc submission failures.
core atproto account, repo, sync, and identity code stays separate from app-view
services. app-view XRPCs are reached through the generic atproto-proxy path;
PDS-owned app state such as app.bsky.actor.getPreferences is stored locally
because that is how the reference PDS and Tranquil treat it.