~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
225 lines · 10.8 KB Raw History
# 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/<cid>` 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/<cid>` 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/<cid>` 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)