A minimal AT Protocol Personal Data Server written in JavaScript.
atproto pds
JavaScript 99.8%
TypeScript 0.2%
113 1 5

Clone this repository

https://tangled.org/chadtmiller.com/pds.js https://tangled.org/did:plc:bcgltzqazw5tb6k2g3ttenbj/pds.js
git@tangled.org:chadtmiller.com/pds.js git@tangled.org:did:plc:bcgltzqazw5tb6k2g3ttenbj/pds.js

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

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#

  1. Install dependencies

    npm install
    
  2. Configure environment

    cp .env.example .env
    # Edit .env with your values
    
  3. Start server

    # Development (auto-reload)
    npm run dev:node
    
    # Production
    cd examples/node && npm start
    
  4. Initialize PDS (only after deploying to a public domain)

    npm run setup -- --pds https://your-hostname.com
    

    Note: 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 HOSTNAME to 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
  1. Create R2 bucket

    wrangler r2 bucket create pds-blobs
    
  2. Create KV namespace

    wrangler kv namespace create SHARED_KV
    
  3. Configure wrangler.toml

    Update examples/cloudflare/wrangler.toml with your KV namespace ID from step 2.

  4. Set secrets

    wrangler secret put PDS_PASSWORD
    wrangler secret put JWT_SECRET
    
  5. Deploy

    cd examples/cloudflare
    wrangler deploy
    
  6. 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