chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
@pdsjs/start
The setup wizard behind start.pdsjs.dev. A visitor signs in with Cloudflare and the wizard deploys pds.js into their own account: one Worker, one R2 bucket, a workers.dev hostname as the handle. The signing key, the server password and the JWT secret are generated in the visitor's browser tab and go from the tab to the visitor's own worker.
The wizard is a static site, served from the pdsjs.dev PDS through the site lexicons like any other site. There is no wizard worker. The page calls its dependencies directly:
- The OAuth token exchange goes to dash.cloudflare.com. It is a form POST, which browsers send without a preflight, and the response carries CORS for the origins registered on the OAuth client.
- PLC registration and the relay crawl request go to plc.directory and bsky.network, which serve open CORS.
- The worker artifact comes from the release site named in
config.json(artifacts.pdsjs.dev, filled by.knotless/workflows/release-worker.ymlon each version tag). Site files serve with open CORS. - Cloudflare API calls are the one thing a browser cannot do:
api.cloudflare.com answers no CORS preflight. They ride through the
serving PDS's
/cf-api/passthrough (PDS_EXPERIMENTAL_CF_API_PROXYin @pdsjs/cloudflare), same-origin because the same worker serves the site.
Operator setup
- Register an OAuth client in the Cloudflare dashboard under Manage
account, then OAuth clients. Grant type: authorization code with PKCE, no
client secret. Redirect URL:
https://<host>/callback. Add the site's origin to Allowed CORS Origins; without it the token exchange fails in the browser. Scopes: account settings read, workers scripts write, workers R2 write; plus zone read and DNS write for the custom-domain picker. Without the zone scopes the picker offers workers.dev alone. - Set
PDS_EXPERIMENTAL_CF_API_PROXY = "true"on the PDS worker that serves the site domain. - Build and publish:
npm run build:start
cd packages/start
PDS_URL=https://pds.pdsjs.dev PDS_IDENTIFIER=pdsjs.dev PDS_PASSWORD=... \
OAUTH_CLIENT_ID=... OAUTH_SCOPES="account-settings.read workers-scripts.write workers-r2.write zone.read dns.write" \
npm run publish-site
The record's fallback serves the page for /callback and /update, the
app's client-side routes. config.json is generated by the publish script;
the page falls back to sane defaults for everything but the client id.
A public OAuth client needs domain ownership verification for the redirect URL's domain, via a DNS TXT record. Public visibility is permanent once set.
What the visitor's account needs
- R2 enabled. Enabling it takes one dashboard step and a billing profile; the wizard links there when the bucket creation fails.
- A workers.dev subdomain. The wizard links to the dashboard when none is set.
Domains
The configure screen offers the account's active zones beside workers.dev.
With a zone chosen, the server answers at pds.<zone> (a worker custom
domain; Cloudflare creates the record and the certificate) and the handle
is the zone apex, proven by a _atproto TXT record the wizard writes. The
domain's site, if any, stays untouched: handle resolution reads the TXT
record and never the apex. The workers.dev hostname stays enabled and
carries the bootstrap, since a fresh certificate can take minutes.
Not yet in the wizard
- Domain registration through the Registrar API (beta; needs a billing profile and a registrant contact on the account).
- Updates for custom-domain servers; /update derives the worker name from a workers.dev hostname only.
- Scheduled backups. The deployed worker supports them; see the root
wrangler.tomlfor the S3 settings.