# pds.js A single-account [AT Protocol](https://atproto.com) Personal Data Server written in JavaScript. Runs on Node.js, Deno, or Cloudflare Workers. Federates with the live network. > **Work in progress.** Experimental. Probably not production-ready yet, though > the author's account, [@chadtmiller.com](https://bsky.app/profile/chadtmiller.com), > runs on it. --- ## What makes this different from the official PDS The [official atproto PDS](https://github.com/bluesky-social/atproto/tree/main/packages/pds) is a multi-tenant server built for running thousands of accounts. pds.js is built around a different premise: **one server, one identity, yours**. That constraint unlocks some things the reference implementation doesn't have: **It runs on the edge.** The Cloudflare Workers build keeps repos in Durable Objects and blobs in R2. No VM, no persistent disk. **It hosts an account interface.** Sign in at `/account` to see what each app has written to your repo, manage passkeys and app passwords, take a CAR backup, or rename your handle — without touching a command line. **It has a site platform.†** Lexicon records stored in your repo can power a public website served from your PDS. No separate hosting required. **It hosts git repositories.†** Push to your PDS over `git-remote-atproto`, clone with stock `git` over read-only HTTP. Repos live as `dev.pdsjs.git.repo` records and chunked blobs in your repo. **It includes a private file store.** Drive† is a blob-backed file manager built into the account interface. Upload files, get public `/.blobs/` links, stream video with seeking. Large files upload across multiple requests and land as a single blob. **It implements permissioned data.†** Private per-user repos shared through *spaces* — a draft proposal on top of atproto. Wire formats may change. See [docs/permissioned-data.md](docs/permissioned-data.md). > † Labs feature — experimental, may change. For everything it covers and everything it doesn't, see the [endpoint comparison](docs/endpoint-comparison.md). --- ## Quick start ```sh git clone https://tangled.org/chadtmiller.com/pds.js cd pds.js pnpm install # must be pnpm — npm fails on workspace:* protocol just dev ``` `just dev` requires [`just`](https://github.com/casey/just) and Docker. It starts a local PLC directory, relay, and Caddy reverse proxy, then runs the PDS and registers a mock account with a few seed records. Open `http://localhost:2471/account` and sign in with the printed credentials. Tear it all down with `just dev-down`. For a manual setup without `just`, or for working on the account UI with live reload, see [CONTRIBUTING.md](CONTRIBUTING.md). --- ## Deploy The fastest path is **[start.pdsjs.dev](https://start.pdsjs.dev)** — a browser wizard that deploys to your own Cloudflare account. It generates your signing key and secrets client-side; nothing sensitive leaves your tab. For manual deployments: | Target | Guide | |---|---| | Docker | [docs/deploy-docker.md](docs/deploy-docker.md) | | Node.js | [docs/deploy-node.md](docs/deploy-node.md) | | Cloudflare Workers | [docs/deploy-cloudflare.md](docs/deploy-cloudflare.md) | | Deno | [docs/deploy-deno.md](docs/deploy-deno.md) | All targets need TLS, a public hostname, and one run of `npm run setup` to register your DID with the PLC directory. --- ## Architecture pds.js uses hexagonal architecture. `@pdsjs/core` holds all business logic and XRPC handlers. It never touches storage directly — it talks through ports. Each platform supplies its own adapters. ``` ┌────────────────┐ space route table ┌───────────────────────────────────┐ │ @pdsjs/spaces │ ────────────────► │ @pdsjs/core │ │ (optional) │ │ (business logic, XRPC handlers) │ └───────┬────────┘ └────────────────┬──────────────────┘ │ │ ▼ ┌───────────────────────┼─────────────────┐ ┌────────────────┐ ▼ ▼ ▼ │SpaceStoragePort│ ┌────────────────┐ ┌─────────────────┐ ┌──────────┐ │ (space repos) │ │ActorStoragePort│ │SharedStoragePort│ │ BlobPort │ └──────┬─────────┘ │ (per-user data)│ │ (global data) │ │ (binary) │ │ └───────┬────────┘ └────────┬────────┘ └────┬─────┘ ┌───┴──┬──────┐ ┌───┴─────┐ ┌────┴─────┐ ┌──┴────┐ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ┌──────┐┌────┐┌──────┐ ┌──────┐ ┌───────┐ ┌──────┐ ┌──────┐ ┌────┐ ┌────┐ │SQLite││ DO ││Memory│ │SQLite│ │Durable│ │SQLite│ │ DO │ │ FS │ │ R2 │ │ ││ ││(test)│ │ │ │Objects│ │ │ │SQLite│ │ │ │ │ └──────┘└────┘└──────┘ └──────┘ └───────┘ └──────┘ └──────┘ └────┘ └────┘ ``` ### Packages | Package | What it does | |---|---| | `@pdsjs/core` | Platform-agnostic business logic and XRPC handlers | | `@pdsjs/node` | Node.js HTTP server with WebSocket support | | `@pdsjs/deno` | Deno HTTP server with WebSocket support | | `@pdsjs/cloudflare` | Cloudflare Workers entrypoint with Durable Objects | | `@pdsjs/storage-sqlite` | SQLite storage adapter (better-sqlite3 or node:sqlite) | | `@pdsjs/blobs-fs` | Filesystem blob storage for Node.js | | `@pdsjs/blobs-deno` | Filesystem blob storage for Deno | | `@pdsjs/blobs-s3` | S3-compatible blob storage | | `@pdsjs/spaces` | Permissioned space repos, credentials, routes (optional) | | `@pdsjs/sites` | Site platform: lexicon-driven public pages served from your repo | | `@pdsjs/git` | Git hosting: push via `git-remote-atproto`, clone over HTTP | | `@pdsjs/readonly` | Read-only server that serves repositories from CAR files | | `@pdsjs/lexicon-resolver` | Lexicon schema resolution and record validation | | `@pdsjs/account-ui` | The account interface (bundled, not a public import) | ### Library usage **Node.js** ```js import { createServer } from '@pdsjs/node' const { listen } = await createServer({ dbPath: './pds.db', blobsDir: './blobs', jwtSecret: process.env.JWT_SECRET, port: 3000, }) await listen() ``` **Deno** ```js import { createServer } from '@pdsjs/deno' const { listen } = await createServer({ dbPath: './pds.db', blobsDir: './blobs', jwtSecret: Deno.env.get('JWT_SECRET'), port: 3000, }) await listen() ``` **Cloudflare Workers** ```js // wrangler.toml points here, or re-export from your own entry export { default, PDSDurableObject } from '@pdsjs/cloudflare' ``` --- ## The account interface Sign in at `/account`. The PDS serves every page itself — no external dashboard. **Your apps** groups the repo by the application that wrote it. Each row shows its sessions and a write history. **Sign-in and security** holds passkeys, active logins, app passwords, the recovery address, and a kill switch that pauses the account. **Drive†** is a private file store backed by blobs. Upload anything from the browser; large files chunk across multiple requests and land as a single blob. Public files get a `/.blobs/` link you can use anywhere an image or video URL works. Drop a video in Drive and it streams with seeking in the example video gallery app. **Sites†** manages installable web apps served from your repo. Each app is a single `.mjs` file that deploys a manifest record and a set of lexicon records; anyone who visits your PDS origin sees the site. > † Labs feature — experimental, may change. --- ## Example apps Four working apps live in `examples/apps/`. Each one discovers whose repo it serves from its own origin, so any of them runs on any pds.js account that installs it. | App | What it does | |---|---| | `photos.mjs` | Photo galleries from `social.grain.*` records — justified layouts, lightbox, map, terrain headers, network favourite counts | | `roasts.mjs` | Coffee roast log — live roast timer, weight-loss and development metrics against a Sweet Maria's roast card, notes and photos | | `drop.mjs` | Image host — drag, paste, or pick a file and the public `/.blobs/` URL copies itself | | `videos.mjs` | Video gallery from Drive† — plays same-origin with seeking, read-only; uploads happen in Drive | Deploy any of them: ```sh PDS_URL=https://pds.example.com \ PDS_DID=did:plc:… \ PDS_APP_PASSWORD=… \ node photos.mjs ``` Then install from the account page's Sites section, or with: ```sh pdsjs-site install at://you.example.com/dev.pdsjs.app.manifest/photos ``` --- ## What it implements pds.js covers the `com.atproto.repo.*`, `com.atproto.sync.*`, `com.atproto.server.*`, and `com.atproto.identity.*` namespaces. That includes record writes, the firehose (`subscribeRepos`), streaming blob upload and ranged download, handle resolution and rename, and account migration in and out. Sessions come from passwords, app passwords, passkeys, or OAuth 2.0 with PKCE and DPoP-bound tokens. There are no `com.atproto.admin.*` endpoints and no invite codes. A single-account server has nobody to administer. The [endpoint comparison](docs/endpoint-comparison.md) lists every endpoint on both sides. --- ## Documentation | Document | Contents | |---|---| | [Configuration](docs/configuration.md) | Every environment variable | | [Architecture](docs/architecture.md) | Ports, adapters, packages, library usage | | [Building an app](docs/apps.md) | Sites, manifests, installs, how to update | | [Permissioned data](docs/permissioned-data.md) | Spaces proposal, the run club example | | [Endpoint comparison](docs/endpoint-comparison.md) | Coverage against the official atproto PDS | | [Scope comparison](docs/scope-comparison.md) | OAuth scopes against the reference implementation | | [Contributing](CONTRIBUTING.md) | Local dev setup, tests, commit gate | --- ## License [MIT](LICENSE)