# 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 | Port | Holds | |------|-------| | `ActorStoragePort` | Per-user data: the repo, preferences, OAuth tokens | | `SharedStoragePort` | Global data: handle resolution, DID mappings | | `BlobPort` | Binary storage: images, videos | | `WebSocketPort` | Real-time subscriptions: `subscribeRepos` | | `SpaceStoragePort` | Permissioned 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: | Field | What core does with it | |-------|------------------------| | `collections` | The 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. | | `routes` | Merged 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` + `handle` | The 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. | | `handleHost` | Answers 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. | | `writeGuard` | Consulted alongside the platform's guard for writes into the public repo. | | `onCommit` | Called after each commit. An error is logged and never fails the write. | | `features` | Merged into the account overview's `features`, which the account application reads to show its sections. | | `stats` | Merged 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 | Package | Description | |---------|-------------| | `@pdsjs/core` | Platform-agnostic business logic and XRPC handlers | | `@pdsjs/spaces` | Permissioned data: space repos, credentials, routes (optional) | | `@pdsjs/lexicon-resolver` | Lexicon schema resolution and record validation | | `@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/readonly` | Read-only server that serves repositories from CAR files | | `@pdsjs/account-ui` | The account interface. Private, and built into a bundle | ## Library usage **Node.js** ```javascript import { createServer } from '@pdsjs/node' const { listen } = await createServer({ dbPath: './pds.db', blobsDir: './blobs', jwtSecret: process.env.JWT_SECRET, port: 3000, }) await listen() ``` **Deno** ```typescript 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** ```javascript // 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](pds-decomposition.md) has the current state and the order of the remaining clusters.