pds.js#
A minimal AT Protocol Personal Data Server written in JavaScript.
Work in progress - This is experimental. You probably shouldn't use this yet.
Features#
- Repo operations (createRecord, getRecord, putRecord, deleteRecord, applyWrites, listRecords)
- Sync endpoints (getRepo, getRecord, subscribeRepos, listRepos, getLatestCommit)
- Auth (createSession, getSession, refreshSession)
- OAuth 2.0 (PAR, authorization code + PKCE, DPoP-bound tokens, refresh, revoke)
- Handle resolution (resolveHandle)
- AppView proxy (app.bsky.* forwarding with service auth)
- Relay notification (requestCrawl)
- Blob storage (uploadBlob, getBlob, listBlobs)
- Single-tenant (multi-tenant hosting on roadmap)
See endpoint comparison for detailed coverage vs the official atproto PDS.
Platform options:
- Node.js - Simple setup, filesystem storage, ideal for self-hosting
- Deno - Modern runtime with built-in TypeScript, uses node:sqlite
- Cloudflare Workers - Edge deployment with Durable Objects and R2
Quick Start (Local Development)#
git clone https://tangled.org/chadtmiller.com/pds.js
cd pds.js && npm install
# Start local PLC + relay (requires Docker)
docker compose up -d
# Start PDS
npm run dev:node
# Register with local PLC (in another terminal)
npm run setup -- --pds https://localhost:3443 --plc-url http://localhost:2582 --relay-url http://localhost:2470
Configuration#
| Variable | Required | Description |
|---|---|---|
| PDS_PASSWORD | Yes | Password for legacy auth and OAuth consent |
| JWT_SECRET | Yes | Secret for signing JWTs |
| PDS_DB_PATH | No | SQLite database path (default: ./pds.db) |
| PDS_BLOBS_DIR | No | Blob storage directory (default: ./blobs) |
| PORT | No | Server port (default: 3000) |
| HOSTNAME | No | Public hostname for the PDS |
| APPVIEW_URL | No | AppView URL for proxying |
| APPVIEW_DID | No | AppView DID for service auth |
| RELAY_URL | No | Relay URL for firehose notifications |
Deploy: Node.js#
-
Install dependencies
npm install -
Configure environment
cp .env.example .env # Edit .env with your values -
Start server
# Development (auto-reload) npm run dev:node # Production cd examples/node && npm start -
Initialize PDS (only after deploying to a public domain)
npm run setup -- --pds https://your-hostname.comNote: This registers your DID with the production PLC directory. Only run this once your PDS is accessible at a public URL.
Production notes:
- Use a reverse proxy (nginx, Caddy) for TLS termination
- Set
HOSTNAMEto your public domain - SQLite database and blobs are stored locally by default
Deploy: Cloudflare Workers#
Prerequisites:
- Cloudflare account with Workers Paid plan (for Durable Objects)
- Wrangler CLI installed
-
Create R2 bucket
wrangler r2 bucket create pds-blobs -
Create KV namespace
wrangler kv namespace create SHARED_KV -
Configure wrangler.toml
Update
examples/cloudflare/wrangler.tomlwith your KV namespace ID from step 2. -
Set secrets
wrangler secret put PDS_PASSWORD wrangler secret put JWT_SECRET -
Deploy
cd examples/cloudflare wrangler deploy -
Initialize PDS
npm run setup -- --pds https://your-worker.workers.dev
Deploy: Deno#
Requires Deno 2.2+ for node:sqlite support.
cd examples/deno
deno run --allow-net --allow-read --allow-write --allow-env main.ts
See examples/deno/README.md for configuration options.
Testing#
npm test # Unit tests
npm run test:e2e:node # E2E against Node.js
npm run test:e2e:deno # E2E against Deno
npm run test:e2e:cloudflare # E2E against Cloudflare
npm run test:coverage # Coverage report
Architecture#
pds.js uses hexagonal architecture with platform-agnostic ports:
┌─────────────────────────────────────┐
│ @pds/core │
│ (business logic, XRPC handlers) │
└──────────────┬──────────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ ActorStoragePort│ │ SharedStoragePort│ │ BlobPort │
│ (per-user data)│ │ (global data) │ │ (binary storage)│
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
┌───────┐ ┌─────────┐ ┌─────────┐ ┌─────┐ ┌─────┐
│SQLite │ │ Durable │ │ KV │ │ FS │ │ R2 │
│ │ │ Objects │ │ │ │ │ │ │
└───────┘ └─────────┘ └─────────┘ └─────┘ └─────┘
Node.js Cloudflare Cloudflare Node.js Cloudflare
Ports:
- ActorStoragePort - Per-user data (repo, preferences, OAuth tokens)
- SharedStoragePort - Global data (handle resolution, DID mappings)
- BlobPort - Binary storage (images, videos)
- WebSocketPort - Real-time subscriptions (subscribeRepos)
Packages#
| Package | Description |
|---|---|
| @pds/core | Platform-agnostic business logic and XRPC handlers |
| @pds/node | Node.js HTTP server with WebSocket support |
| @pds/deno | Deno HTTP server with WebSocket support |
| @pds/cloudflare | Cloudflare Workers entry point with Durable Objects |
| @pds/storage-sqlite | SQLite storage adapter (better-sqlite3 or node:sqlite) |
| @pds/blobs-fs | Filesystem blob storage for Node.js |
| @pds/blobs-deno | Filesystem blob storage for Deno |
| @pds/blobs-s3 | S3-compatible blob storage |
Node.js usage:
import { createServer } from '@pds/node'
const { listen } = await createServer({
dbPath: './pds.db',
blobsDir: './blobs',
jwtSecret: process.env.JWT_SECRET,
port: 3000,
})
await listen()
Cloudflare usage:
// Re-export from @pds/cloudflare (or point wrangler.toml directly at it)
export { default, PDSDurableObject } from '@pds/cloudflare'
Deno usage:
import { createServer } from '@pds/deno'
const { listen } = await createServer({
dbPath: './pds.db',
blobsDir: './blobs',
jwtSecret: Deno.env.get('JWT_SECRET'),
port: 3000,
})
await listen()
Contributing#
Before submitting a PR:
npm run check # Biome lint + format check
npm run typecheck # TypeScript
npm test # Unit tests
Development commands:
npm run dev:node # Run Node.js dev server
npm run dev:cloudflare # Run Cloudflare dev server
npm run format # Auto-format code
npm run lint # Run linter
Project structure:
packages/
core/ # Platform-agnostic core
node/ # Node.js adapter
deno/ # Deno adapter
cloudflare/ # Cloudflare adapter
storage-*/ # Storage implementations
blobs-*/ # Blob storage implementations
examples/
node/ # Node.js example
deno/ # Deno example
cloudflare/ # Cloudflare example