chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
App Platform — Phase 1: Serve Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Serve static sites from the account repo (blobs + a dev.pdsjs.site.deploy manifest record) on the deployment's apex and one-label subdomains, via a new @pdsjs/sites package and one hostname-dispatch seam in core, with a deploy CLI and an account-UI Sites page.
Architecture: New workspace package packages/sites, workspace-only dependency on @pdsjs/core. Core gains config.siteHandler / config.siteBrowser and one hostname check at the top of fetch(); platform packages gate on PDS_SITE_DOMAIN and compose the handler from ports. Site content never renders on the PDS origin.
Tech Stack: plain ESM JS + JSDoc types, vitest, biome (single quotes, 2-space indent), one commit per task.
Global Constraints
- Spec:
docs/superpowers/specs/2026-08-06-app-platform-serve.md. @pdsjs/siteshas zero external dependencies; workspace-only dependency on@pdsjs/core.src/cli.jsis the only file allowed to importnode:*modules, and nothing on the serve path imports it.@pdsjs/coremust never import@pdsjs/sites. Dependency direction is one-way.- No dependency on
@pdsjs/spacesin any direction; everything works withPDS_ENABLE_SPACESoff. - No
anyin library type annotations;/** @type {any} */casts are allowed only in test mocks. - Tests at
packages/sites/test/*.test.js(vitest already includes them). CI runs biome +tsc --noEmit+ unit tests only — unit coverage gates everything. - The flag-off path is load-bearing: with no
siteHandler, foreign-Host requests behave byte-identically to today. Explicit test. - Branch:
app-platform-serve.
Task 1: Spec + scaffolding + hostname resolution
Create packages/sites/{package.json, src/index.js, src/hostname.js, src/lexicons.js} and test/hostname.test.js (apex match, valid/invalid labels, case folding, 63-char bounds, multi-label, unrelated domains → null).
Task 2: Manifest loading
src/manifest.js: getRecord → cborDecode → one-hop base merge (refuse deeper) → Map<path, entry>; module-level LRU (≤32) keyed by record CID. Tests with an in-memory actorStorage mock built on cborEncodeDagCbor.
Task 3: Serve handler
src/serve.js + src/handler.js (createSiteHandler(ctx)), per the spec's serving semantics §3. Tests: null on unrelated host; unknown-site 404; index/trailing-slash/301; traversal + bad-encoding 400s; SPA fallback 200; ETag/304 and HEAD asserted to make zero blob reads (mock call counts); MIME precedence; cacheControl override; 405s incl. OPTIONS; /.well-known/atproto-did.
Task 4: Core seam
packages/core/src/pds.js: siteHandler/siteBrowser constructor options, sitesEnabled, hostname check at top of fetch() (before the OPTIONS short-circuit, outside withDpopNonce, no addCorsHeaders, before the account-app intercept). packages/core/src/ports.js: SiteBrowserPort. Seam tests via pds.fetch with foreign Host headers per spec §10; audit that no fall-through path derives OAuth issuer URLs from url.host.
Task 5: Site browser + account API
src/browser.js; core handleApiSites / handleApiSiteDelete via the accountApi wrapper, route-table lines, features.sites. Tests in the account-page style + a readOnly-mode test.
Task 6: Cloudflare wiring
Env typedef (PDS_SITE_DOMAIN, PDS_SITE_APEX_NAME), DO-constructor gate next to spaces, config passthrough; examples/cloudflare/wrangler.toml route/DNS comments; .dev.vars note. Tests: flag-off/flag-on via the d1-shim harness.
Task 7: Node/Deno wiring
Dynamic import behind PDS_SITE_DOMAIN, optional peer dep, README notes (reverse proxy, wildcard certs). Tests: flag-off/flag-on.
Task 8: Deploy CLI
src/deploy.js pure helpers (walk/diff/record-build) + src/cli.js (pdsjs-site deploy). Tests: CID-stable diff/skip, oversize abort, record-size abort, MIME assignment.
Task 9: Account-UI Sites page
packages/account-ui/src/pages/sites.jsx, app.jsx wiring (sectionsFor/PAGES/dispatch), queryKeys.sites; rebuild assets.
Task 10: README
Root README package table, architecture diagram arrow (platforms → sites → core), env-var docs.
Later phases (not this plan)
Grant (install gate + consent skip), store/install flow + Store UI, Query, React, Schedule — sketched in the spec §9.