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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
145 lines · 8.4 KB Raw History

Architecture

pds.js uses hexagonal architecture. @pdsjs/core holds the business logic and the XRPC handlers, and it reaches storage only through ports. Each platform supplies its own adapters.

 ┌────────────────┐ space route table ┌───────────────────────────────────┐
 │ @pdsjs/spaces  │ ────────────────► │            @pdsjs/core            │
 │ (optional)     │ (the platform     │   (business logic, XRPC handlers) │
 └───────┬────────┘  package wires it)└────────────────┬──────────────────┘
         │                                             │
         ▼                     ┌───────────────────────┼─────────────────┐
┌────────────────┐             ▼                       ▼                 ▼
│SpaceStoragePort│    ┌────────────────┐     ┌─────────────────┐   ┌──────────┐
│  (space repos) │    │ActorStoragePort│     │SharedStoragePort│   │ BlobPort │
└──────┬─────────┘    │ (per-user data)│     │  (global data)  │   │ (binary) │
       │              └───────┬────────┘     └────────┬────────┘   └────┬─────┘
   ┌───┴──┬──────┐        ┌───┴─────┐            ┌────┴─────┐        ┌──┴────┐
   ▼      ▼      ▼        ▼         ▼            ▼          ▼        ▼       ▼
┌──────┐┌────┐┌──────┐ ┌──────┐ ┌───────┐    ┌──────┐  ┌──────┐   ┌────┐ ┌────┐
│SQLite││ DO ││Memory│ │SQLite│ │Durable│    │SQLite│  │  DO  │   │ FS │ │ R2 │
│      ││    ││(test)│ │      │ │Objects│    │      │  │SQLite│   │    │ │    │
└──────┘└────┘└──────┘ └──────┘ └───────┘    └──────┘  └──────┘   └────┘ └────┘

Ports

PortHolds
ActorStoragePortPer-user data: the repo, preferences, OAuth tokens
SharedStoragePortGlobal data: handle resolution, DID mappings
BlobPortBinary storage: images, videos
WebSocketPortReal-time subscriptions: subscribeRepos
SpaceStoragePortPermissioned space data, when spaces are enabled

The port types are in @pdsjs/core/ports. Annotate against those types rather than redeclaring the row shapes.

Optional spaces

@pdsjs/core never imports @pdsjs/spaces. When PDS_ENABLE_SPACES is set, the platform packages (@pdsjs/node, @pdsjs/cloudflare) build the space storage port, which is platform-specific, pass it to core as spaceStorage, and mount createSpacesExtension from @pdsjs/spaces/extension. Core builds the account page's space browser over the storage; the extension serves the space endpoints and the account page's space routes. Without the flag the space endpoints do not exist, and a deployment never installs the package.

Extensions

A feature package mounts on the server as an extension. @pdsjs/core never imports the package; the platform passes a factory in through the extensions option, and core calls it with an ExtensionHost of late-binding functions (getDid, xrpc, createCommit, accountApi) plus the storage ports. The extension it returns contributes any of:

FieldWhat core does with it
collectionsThe record collections the extension owns. The host's actorStorage, spaceStorage, spaceBrowser and createCommit refuse a record call or commit op naming any other; ['*'] allows every collection, and an absent list allows none. Blobs, blocks, the event log and the account's own fields are not scoped, and xrpc is anonymous: credentials on a request an extension passes in are removed.
routesMerged into the route table. A path core already serves is refused at construction. Handlers are called unbound; an extension reaches the server through its host, never through this.
prefixes + handleThe handler answers requests under its declared prefixes (/npm, say) before core dispatch, for surfaces outside XRPC. The extension owns the whole response. A prefix core serves under (/xrpc, /oauth, /account, /.well-known, or any route in its table) or that overlaps another extension's is refused at construction.
handleHostAnswers requests addressed to a hostname other than the server's (a static site, say), after prefix handlers and before core dispatch. The extension owns the whole response.
writeGuardConsulted alongside the platform's guard for writes into the public repo.
onCommitCalled after each commit. An error is logged and never fails the write.
featuresMerged into the account overview's features, which the account application reads to show its sections.
statsMerged into the overview's stats, the counts beside those sections, from a count(collection) core computes across the public repo and every live space.

Both types are in @pdsjs/core/ports as ExtensionFactory and PdsExtension. The npm and OCI registries, git hosting, the drive and sites are on this seam (createNpmExtension, createOciExtension, createGitExtension, createDriveExtension, createSitesExtension, each from its package's /extension export), and each serves its own /account/api routes through the host's accountApi. The ExtensionHost carries spaceStorage and spaceBrowser when spaces are on, so an extension whose records may be held in a space reads them there; one that manages spaces builds the admin methods it needs over that storage from @pdsjs/spaces, the way git and drive do. Every feature package is on the seam; spaceStorage is the one feature-shaped option core keeps, since the storage port is the platform's.

Packages

PackageDescription
@pdsjs/corePlatform-agnostic business logic and XRPC handlers
@pdsjs/spacesPermissioned data: space repos, credentials, routes (optional)
@pdsjs/lexicon-resolverLexicon schema resolution and record validation
@pdsjs/nodeNode.js HTTP server with WebSocket support
@pdsjs/denoDeno HTTP server with WebSocket support
@pdsjs/cloudflareCloudflare Workers entrypoint with Durable Objects
@pdsjs/storage-sqliteSQLite storage adapter (better-sqlite3 or node:sqlite)
@pdsjs/blobs-fsFilesystem blob storage for Node.js
@pdsjs/blobs-denoFilesystem blob storage for Deno
@pdsjs/blobs-s3S3-compatible blob storage
@pdsjs/readonlyRead-only server that serves repositories from CAR files
@pdsjs/account-uiThe account interface. Private, and built into a bundle

Library usage

Node.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

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

// Re-export from @pdsjs/cloudflare (or point wrangler.toml directly at it)
export { default, PDSDurableObject } from '@pdsjs/cloudflare'

Handler decomposition

core/src/pds.js becomes one handler module per cluster, under core/src/handlers/. Each module exports a createXHandlers(ctx) factory. The factory takes an explicit context and returns a route table fragment, and the constructor merges the fragment into this.routes.

pds-decomposition.md has the current state and the order of the remaining clusters.