A minimal AT Protocol Personal Data Server written in JavaScript.
at main 259 lines 8.6 kB view raw view rendered
1# pds.js 2 3A minimal AT Protocol Personal Data Server written in JavaScript. 4 5> **Work in progress** - This is experimental. You probably shouldn't use this yet. 6 7## Features 8 9- Repo operations (createRecord, getRecord, putRecord, deleteRecord, applyWrites, listRecords) 10- Sync endpoints (getRepo, getRecord, subscribeRepos, listRepos, getLatestCommit) 11- Auth (createSession, getSession, refreshSession) 12- OAuth 2.0 (PAR, authorization code + PKCE, DPoP-bound tokens, refresh, revoke) 13- Handle resolution (resolveHandle) 14- AppView proxy (app.bsky.* forwarding with service auth) 15- Relay notification (requestCrawl) 16- Blob storage (uploadBlob, getBlob, listBlobs) 17- Single-tenant (multi-tenant hosting on roadmap) 18 19See [endpoint comparison](docs/endpoint-comparison.md) for detailed coverage vs the official atproto PDS. 20 21**Platform options:** 22- **Node.js** - Simple setup, filesystem storage, ideal for self-hosting 23- **Deno** - Modern runtime with built-in TypeScript, uses node:sqlite 24- **Cloudflare Workers** - Edge deployment with Durable Objects and R2 25 26## Quick Start (Local Development) 27 28```bash 29git clone https://tangled.org/chadtmiller.com/pds.js 30cd pds.js && npm install 31 32# Start local PLC + relay (requires Docker) 33docker compose up -d 34 35# Start PDS 36npm run dev:node 37 38# Register with local PLC (in another terminal) 39npm run setup -- --pds https://localhost:3443 --plc-url http://localhost:2582 --relay-url http://localhost:2470 40``` 41 42## Configuration 43 44| Variable | Required | Description | 45|----------|----------|-------------| 46| PDS_PASSWORD | Yes | Password for legacy auth and OAuth consent | 47| JWT_SECRET | Yes | Secret for signing JWTs | 48| PDS_DB_PATH | No | SQLite database path (default: ./pds.db) | 49| PDS_BLOBS_DIR | No | Blob storage directory (default: ./blobs) | 50| PORT | No | Server port (default: 3000) | 51| HOSTNAME | No | Public hostname for the PDS | 52| APPVIEW_URL | No | AppView URL for proxying | 53| APPVIEW_DID | No | AppView DID for service auth | 54| RELAY_URL | No | Relay URL for firehose notifications | 55 56## Deploy: Node.js 57 581. **Install dependencies** 59 ```bash 60 npm install 61 ``` 62 632. **Configure environment** 64 ```bash 65 cp .env.example .env 66 # Edit .env with your values 67 ``` 68 693. **Start server** 70 ```bash 71 # Development (auto-reload) 72 npm run dev:node 73 74 # Production 75 cd examples/node && npm start 76 ``` 77 784. **Initialize PDS** (only after deploying to a public domain) 79 ```bash 80 npm run setup -- --pds https://your-hostname.com 81 ``` 82 83 > **Note:** This registers your DID with the production PLC directory. Only run this once your PDS is accessible at a public URL. 84 85**Production notes:** 86- Use a reverse proxy (nginx, Caddy) for TLS termination 87- Set `HOSTNAME` to your public domain 88- SQLite database and blobs are stored locally by default 89 90## Deploy: Cloudflare Workers 91 92**Prerequisites:** 93- Cloudflare account with Workers Paid plan (for Durable Objects) 94- Wrangler CLI installed 95 961. **Create R2 bucket** 97 ```bash 98 wrangler r2 bucket create pds-blobs 99 ``` 100 1012. **Create KV namespace** 102 ```bash 103 wrangler kv namespace create SHARED_KV 104 ``` 105 1063. **Configure wrangler.toml** 107 108 Update `examples/cloudflare/wrangler.toml` with your KV namespace ID from step 2. 109 1104. **Set secrets** 111 ```bash 112 wrangler secret put PDS_PASSWORD 113 wrangler secret put JWT_SECRET 114 ``` 115 1165. **Deploy** 117 ```bash 118 cd examples/cloudflare 119 wrangler deploy 120 ``` 121 1226. **Initialize PDS** 123 ```bash 124 npm run setup -- --pds https://your-worker.workers.dev 125 ``` 126 127## Deploy: Deno 128 129Requires Deno 2.2+ for `node:sqlite` support. 130 131```bash 132cd examples/deno 133deno run --allow-net --allow-read --allow-write --allow-env main.ts 134``` 135 136See [examples/deno/README.md](examples/deno/README.md) for configuration options. 137 138## Testing 139 140```bash 141npm test # Unit tests 142npm run test:e2e:node # E2E against Node.js 143npm run test:e2e:deno # E2E against Deno 144npm run test:e2e:cloudflare # E2E against Cloudflare 145npm run test:coverage # Coverage report 146``` 147 148## Architecture 149 150pds.js uses hexagonal architecture with platform-agnostic ports: 151 152``` 153 ┌─────────────────────────────────────┐ 154 │ @pds/core │ 155 │ (business logic, XRPC handlers) │ 156 └──────────────┬──────────────────────┘ 157158 ┌────────────────────┼────────────────────┐ 159 │ │ │ 160 ▼ ▼ ▼ 161 ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 162 │ ActorStoragePort│ │ SharedStoragePort│ │ BlobPort │ 163 │ (per-user data)│ │ (global data) │ │ (binary storage)│ 164 └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ 165 │ │ │ 166 ┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐ 167 │ │ │ │ │ │ 168 ▼ ▼ ▼ ▼ ▼ ▼ 169 ┌───────┐ ┌─────────┐ ┌─────────┐ ┌─────┐ ┌─────┐ 170 │SQLite │ │ Durable │ │ KV │ │ FS │ │ R2 │ 171 │ │ │ Objects │ │ │ │ │ │ │ 172 └───────┘ └─────────┘ └─────────┘ └─────┘ └─────┘ 173 Node.js Cloudflare Cloudflare Node.js Cloudflare 174``` 175 176**Ports:** 177- **ActorStoragePort** - Per-user data (repo, preferences, OAuth tokens) 178- **SharedStoragePort** - Global data (handle resolution, DID mappings) 179- **BlobPort** - Binary storage (images, videos) 180- **WebSocketPort** - Real-time subscriptions (subscribeRepos) 181 182## Packages 183 184| Package | Description | 185|---------|-------------| 186| @pds/core | Platform-agnostic business logic and XRPC handlers | 187| @pds/node | Node.js HTTP server with WebSocket support | 188| @pds/deno | Deno HTTP server with WebSocket support | 189| @pds/cloudflare | Cloudflare Workers entry point with Durable Objects | 190| @pds/storage-sqlite | SQLite storage adapter (better-sqlite3 or node:sqlite) | 191| @pds/blobs-fs | Filesystem blob storage for Node.js | 192| @pds/blobs-deno | Filesystem blob storage for Deno | 193| @pds/blobs-s3 | S3-compatible blob storage | 194 195**Node.js usage:** 196```javascript 197import { createServer } from '@pds/node' 198 199const { listen } = await createServer({ 200 dbPath: './pds.db', 201 blobsDir: './blobs', 202 jwtSecret: process.env.JWT_SECRET, 203 port: 3000, 204}) 205 206await listen() 207``` 208 209**Cloudflare usage:** 210```javascript 211// Re-export from @pds/cloudflare (or point wrangler.toml directly at it) 212export { default, PDSDurableObject } from '@pds/cloudflare' 213``` 214 215**Deno usage:** 216```typescript 217import { createServer } from '@pds/deno' 218 219const { listen } = await createServer({ 220 dbPath: './pds.db', 221 blobsDir: './blobs', 222 jwtSecret: Deno.env.get('JWT_SECRET'), 223 port: 3000, 224}) 225 226await listen() 227``` 228 229## Contributing 230 231**Before submitting a PR:** 232```bash 233npm run check # Biome lint + format check 234npm run typecheck # TypeScript 235npm test # Unit tests 236``` 237 238**Development commands:** 239```bash 240npm run dev:node # Run Node.js dev server 241npm run dev:cloudflare # Run Cloudflare dev server 242npm run format # Auto-format code 243npm run lint # Run linter 244``` 245 246**Project structure:** 247``` 248packages/ 249 core/ # Platform-agnostic core 250 node/ # Node.js adapter 251 deno/ # Deno adapter 252 cloudflare/ # Cloudflare adapter 253 storage-*/ # Storage implementations 254 blobs-*/ # Blob storage implementations 255examples/ 256 node/ # Node.js example 257 deno/ # Deno example 258 cloudflare/ # Cloudflare example 259```