
Security News
CVE Volume Surges Past 48,000 in 2025 as WordPress Plugin Ecosystem Drives Growth
CVE disclosures hit a record 48,185 in 2025, driven largely by vulnerabilities in third-party WordPress plugins.
@ascorbic/pds
Advanced tools
🚨 This package has been renamed to
@getcirrus/pdsThis package is deprecated and will no longer receive updates. Please migrate to
@getcirrus/pdsfor the latest features and bug fixes.
A single-user AT Protocol Personal Data Server (PDS) that runs on Cloudflare Workers. Host your own Bluesky identity with minimal infrastructure.
⚠️ Experimental Software
This is an early-stage project under active development. Do not migrate your main Bluesky account to this PDS yet. Use a test account or create a new identity for experimentation. Data loss, breaking changes, and missing features are expected.
A Personal Data Server is where your Bluesky data lives – your posts, follows, profile, and media. This package lets you run your own PDS on Cloudflare Workers, giving you control over your data and identity.
Key benefits:
npm create pds
This scaffolds a new project, installs dependencies, and runs the setup wizard. Start the dev server:
cd pds-worker
npm run dev
npm install @ascorbic/pds
// src/index.ts
export { default, AccountDurableObject } from "@ascorbic/pds";
{
"name": "my-pds",
"main": "src/index.ts",
"compatibility_date": "2024-12-01",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [{ "name": "ACCOUNT", "class_name": "AccountDurableObject" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["AccountDurableObject"] }],
"r2_buckets": [{ "binding": "BLOBS", "bucket_name": "pds-blobs" }]
}
npx pds init
This prompts for your hostname, handle, and password, then generates signing keys and writes configuration.
The package includes a CLI for setup, migration, and secret management.
pds initInteractive setup wizard for configuring the PDS.
pds init # Configure for local development
pds init --production # Deploy secrets to Cloudflare
What it does:
wrangler.jsonc.dev.vars (local) or Cloudflare (production)For migrations, it detects existing accounts and configures the PDS in deactivated mode, ready for data import.
pds migrateTransfers account data from an existing PDS to a new one.
pds migrate # Migrate to production PDS
pds migrate --dev # Migrate to local development server
pds migrate --clean # Reset and start fresh migration
What it does:
The migration is resumable. If interrupted, run pds migrate again to continue.
Flags:
--dev – Target the local development server instead of production--clean – Delete any existing imported data and start fresh (only works on deactivated accounts)pds activateEnables writes on the account after migration.
pds activate # Activate production account
pds activate --dev # Activate local development account
Run this after migrating data and updating the DID document to point to the new PDS. The account will start accepting new posts, follows, and other writes.
pds deactivateDisables writes on the account.
pds deactivate # Deactivate production account
pds deactivate --dev # Deactivate local development account
Use this before re-importing data (for example, to recover from issues). Deactivating prevents new writes during the reset and re-migration.
After deactivating:
pds migrate --clean # Reset and re-import
pds activate # Go live again
pds secretManage individual secrets.
pds secret key # Generate new signing keypair
pds secret jwt # Generate new JWT secret
pds secret password # Set account password
All secret commands support:
--local – Write to .dev.vars instead of Cloudflarepds secret keyGenerates a new secp256k1 signing keypair. Updates both the private key secret and the public key in your configuration.
pds secret jwtGenerates a new JWT signing secret for session tokens.
pds secret passwordPrompts for a new password and stores the bcrypt hash.
The PDS runs as a Cloudflare Worker with a Durable Object for state:
┌─────────────────────────────────────────────────────────────┐
│ Cloudflare Worker │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Hono Router │ │
│ │ • Authentication middleware │ │
│ │ • CORS handling │ │
│ │ • DID document serving │ │
│ │ • XRPC endpoint routing │ │
│ │ • OAuth 2.1 provider │ │
│ │ • Proxy to AppView for read endpoints │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ AccountDurableObject │ │
│ │ • SQLite repository storage │ │
│ │ • Merkle tree for commits │ │
│ │ • Record indexing │ │
│ │ • WebSocket firehose │ │
│ │ • OAuth token storage │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ R2 Bucket │ │
│ │ • Blob storage (images, videos) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
For endpoints this PDS doesn't implement directly (like feed generation or notifications), requests are proxied to the Bluesky AppView. The PDS signs these requests with service authentication, so you get full Bluesky functionality without implementing every endpoint.
AT Protocol uses two types of identifiers:
did:web:pds.example.com or did:plc:abc123). This never changes and is tied to a signing key.alice.example.com). This can be any domain under the owner's control.The DID document (served at /.well-known/did.json) contains the public key and tells the network where the PDS is. The alsoKnownAs field links the DID to the handle.
/.well-known/did.jsonBluesky verifies control of the handle domain. Two methods are available:
When the handle matches the PDS hostname (for example, both are pds.example.com), the PDS automatically serves /.well-known/atproto-did with the DID. No additional DNS setup required.
For a handle on a different domain (for example, handle alice.example.com while PDS is at pds.example.com):
_atproto.alice.example.com TXT "did=did:web:pds.example.com"
dig TXT _atproto.alice.example.com
The PDS uses environment variables for configuration. Public values go in wrangler.jsonc, secrets are stored via Wrangler or in .dev.vars for local development.
| Variable | Description |
|---|---|
PDS_HOSTNAME | Public hostname (e.g., pds.example.com) |
DID | Account DID (did:web:... or did:plc:...) |
HANDLE | Account handle |
SIGNING_KEY_PUBLIC | Public key for DID document (multibase) |
INITIAL_ACTIVE | Whether account starts active (true/false) |
| Variable | Description |
|---|---|
AUTH_TOKEN | Bearer token for API write operations |
SIGNING_KEY | Private signing key (secp256k1 JWK) |
JWT_SECRET | Secret for signing session JWTs |
PASSWORD_HASH | Bcrypt hash of password for app login |
| Endpoint | Description |
|---|---|
GET /.well-known/did.json | DID document for did:web resolution |
GET /.well-known/atproto-did | Handle verification (only if handle matches hostname) |
GET /health | Health check with version info |
| Endpoint | Description |
|---|---|
GET /xrpc/com.atproto.sync.getRepo | Export repository as CAR file |
GET /xrpc/com.atproto.sync.getRepoStatus | Repository status (commit, rev) |
GET /xrpc/com.atproto.sync.getBlocks | Get specific blocks from repository |
GET /xrpc/com.atproto.sync.getBlob | Download a blob by CID |
GET /xrpc/com.atproto.sync.listRepos | List repositories (single-user: just yours) |
GET /xrpc/com.atproto.sync.listBlobs | List all blobs in repository |
GET /xrpc/com.atproto.sync.subscribeRepos | WebSocket firehose for real-time updates |
| Endpoint | Auth | Description |
|---|---|---|
GET /xrpc/com.atproto.repo.describeRepo | No | Repository metadata |
GET /xrpc/com.atproto.repo.getRecord | No | Get a single record |
GET /xrpc/com.atproto.repo.listRecords | No | List records in a collection |
POST /xrpc/com.atproto.repo.createRecord | Yes | Create a new record |
POST /xrpc/com.atproto.repo.putRecord | Yes | Create or update a record |
POST /xrpc/com.atproto.repo.deleteRecord | Yes | Delete a record |
POST /xrpc/com.atproto.repo.applyWrites | Yes | Batch create/update/delete operations |
POST /xrpc/com.atproto.repo.uploadBlob | Yes | Upload an image or video |
POST /xrpc/com.atproto.repo.importRepo | Yes | Import repository from CAR file |
GET /xrpc/com.atproto.repo.listMissingBlobs | Yes | List blobs referenced but not yet uploaded |
| Endpoint | Auth | Description |
|---|---|---|
GET /xrpc/com.atproto.server.describeServer | No | Server capabilities and info |
POST /xrpc/com.atproto.server.createSession | No | Login with password, get JWT |
POST /xrpc/com.atproto.server.refreshSession | Yes | Refresh JWT tokens |
GET /xrpc/com.atproto.server.getSession | Yes | Get current session info |
POST /xrpc/com.atproto.server.deleteSession | Yes | Logout |
GET /xrpc/com.atproto.server.getServiceAuth | Yes | Get JWT for external services |
GET /xrpc/com.atproto.server.getAccountStatus | Yes | Account status (active/deactivated) |
POST /xrpc/com.atproto.server.activateAccount | Yes | Enable writes |
POST /xrpc/com.atproto.server.deactivateAccount | Yes | Disable writes |
| Endpoint | Description |
|---|---|
GET /xrpc/com.atproto.identity.resolveHandle | Resolve handle to DID (local or proxied) |
| Endpoint | Auth | Description |
|---|---|---|
GET /xrpc/app.bsky.actor.getPreferences | Yes | Get user preferences |
POST /xrpc/app.bsky.actor.putPreferences | Yes | Set user preferences |
The PDS includes a complete OAuth 2.1 provider for "Login with Bluesky":
| Endpoint | Description |
|---|---|
GET /.well-known/oauth-authorization-server | OAuth server metadata |
POST /oauth/par | Pushed Authorization Request |
GET /oauth/authorize | Authorization endpoint |
POST /oauth/authorize | Process authorization decision |
POST /oauth/token | Token exchange |
POST /oauth/revoke | Token revocation |
See the @ascorbic/atproto-oauth-provider package for implementation details.
Enable R2 in your Cloudflare dashboard. The bucket will be created automatically on first deploy.
Run the production setup to deploy secrets:
npx pds init --production
wrangler deploy
Moving an existing Bluesky account to your own PDS:
npx pds init
# Answer "Yes" when asked about migrating an existing account
wrangler deploy
npx pds migrate
Follow the AT Protocol account migration guide to update your DID document. This typically requires email verification from your current PDS.
npx pds activate
Records are validated against AT Protocol lexicon schemas before being stored. The PDS uses optimistic validation:
This allows the PDS to accept records for new or custom collection types while still enforcing validation for known types like app.bsky.feed.post.
MIT
FAQs
AT Protocol PDS on Cloudflare Workers
We found that @ascorbic/pds demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
CVE disclosures hit a record 48,185 in 2025, driven largely by vulnerabilities in third-party WordPress plugins.

Security News
Socket CEO Feross Aboukhadijeh joins Insecure Agents to discuss CVE remediation and why supply chain attacks require a different security approach.

Security News
Tailwind Labs laid off 75% of its engineering team after revenue dropped 80%, as LLMs redirect traffic away from documentation where developers discover paid products.