forked from
chadtmiller.com/pds.js
A minimal AT Protocol Personal Data Server written in JavaScript.
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 └──────────────┬──────────────────────┘
157 │
158 ┌────────────────────┼────────────────────┐
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```