Your Personal PDS#
A single-user AT Protocol Personal Data Server running on Cloudflare Workers.
⚠️ Beta Software
This is under active development. Account migration has been tested and works, but breaking changes may still occur. Consider backing up important data before migrating a primary account.
Getting Started#
1. Install dependencies#
pnpm install
# or: npm install / yarn install
2. Configure the PDS#
Run the setup wizard if not already done:
pnpm pds init
This prompts for:
- PDS hostname – The deployment domain (e.g.,
pds.example.com) - Handle – The Bluesky username (e.g.,
alice.example.com) - Password – For logging in from Bluesky apps
The wizard generates cryptographic keys and writes configuration to .dev.vars and wrangler.jsonc.
3. Run locally#
pnpm dev
The PDS is now running at http://localhost:5173. Test it with:
curl http://localhost:5173/health
curl http://localhost:5173/.well-known/did.json
4. Deploy to production#
When running pds init, answer "Yes" when asked if you want to deploy to Cloudflare. This pushes secrets to Cloudflare Workers.
Then deploy the worker:
pnpm run deploy
Finally, configure DNS to point your domain to the worker.
Migrating an Existing Account#
To move an existing Bluesky account from bsky.social or another PDS:
Step 1: Configure for migration#
pnpm pds init
# Answer "Yes" when asked about migrating an existing account
This detects your existing account, generates new signing keys, and configures the PDS in deactivated mode.
Step 2: Deploy and transfer data#
pnpm run deploy # Deploy the worker
pnpm pds migrate # Transfer data from source PDS
The migrate command downloads the repository (posts, follows, likes) and all images/videos from the current PDS. If interrupted, run it again to resume.
Step 3: Update your identity#
pnpm pds identity
This updates your DID document to point to your new PDS. You'll need to:
- Enter your password for the source PDS
- Enter the confirmation token sent to your email
Step 4: Activate the account#
pnpm pds activate
This enables writes on your new PDS. Your account is now live.
Step 5: Verify the migration#
pnpm pds status
Check that the account is active and your handle resolves correctly.
Full command sequence#
pnpm pds init # Configure + deploy secrets (answer "Yes" to deploy)
pnpm run deploy # Deploy the worker
pnpm pds migrate # Transfer data from source PDS
pnpm pds identity # Update DID document (requires email)
pnpm pds activate # Enable writes
pnpm pds status # Verify everything is working
CLI Commands#
| Command | Description |
|---|---|
pnpm pds init |
Interactive setup wizard (prompts for Cloudflare deploy) |
pnpm pds migrate |
Transfer account from source PDS |
pnpm pds migrate --clean |
Reset and re-import data |
pnpm pds identity |
Update DID document to point to new PDS |
pnpm pds activate |
Enable writes (go live) |
pnpm pds deactivate |
Disable writes (for re-import) |
pnpm pds status |
Check account and repository status |
pnpm pds passkey add |
Register a passkey for passwordless login |
pnpm pds secret key |
Generate new signing keypair |
pnpm pds secret jwt |
Generate new JWT secret |
pnpm pds secret password |
Set account password |
Add --dev to target your local development server instead of production.
Configuration#
Public Variables (wrangler.jsonc)#
| Variable | Description |
|---|---|
PDS_HOSTNAME |
Public hostname (e.g., pds.example.com) |
DID |
Account DID |
HANDLE |
Account handle |
SIGNING_KEY_PUBLIC |
Public key for DID document |
Secrets (.dev.vars or Cloudflare)#
| Variable | Description |
|---|---|
AUTH_TOKEN |
Bearer token for API write operations |
SIGNING_KEY |
Private signing key |
JWT_SECRET |
Secret for session tokens |
PASSWORD_HASH |
Bcrypt hash of the account password |
Handle Verification#
Bluesky verifies control of the handle domain.
If the handle matches the PDS hostname (for example, both are pds.example.com):
- No extra setup needed. The PDS handles verification automatically.
If the handle is on a different domain (for example, handle alice.example.com, PDS at pds.example.com):
Add a DNS TXT record:
_atproto.alice.example.com TXT "did=did:web:pds.example.com"
Verify with:
dig TXT _atproto.alice.example.com
Project Structure#
├── src/
│ └── index.ts # Worker entry point (re-exports PDS)
├── wrangler.jsonc # Cloudflare Worker configuration
├── .dev.vars # Local secrets (not committed)
└── package.json
Troubleshooting#
"PDS not responding"#
Ensure the worker is deployed (pnpm run deploy) or the dev server is running (pnpm dev).
"Failed to resolve handle"#
Check the handle configuration:
- For DNS verification: ensure the TXT record has propagated (
dig TXT _atproto.yourhandle.com) - For same-domain handles: ensure the PDS is accessible at
https://yourdomain.com/.well-known/atproto-did
Migration issues#
If migration fails partway through:
- Run
pnpm pds migrateagain to resume from where you left off - Use
pnpm pds migrate --cleanto start fresh (only on deactivated accounts)
Resources#
- AT Protocol Documentation
- Cloudflare Workers Docs
- @getcirrus/pds Documentation
- Account Migration Guide
License#
MIT