chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
Deploy: Cloudflare Workers
The Worker keeps the repo and the shared account data in a Durable Object with SQLite storage, and blobs in R2.
Prerequisites
- A Cloudflare account, signed in with
npx wrangler login
The Workers Free plan carries Durable Objects with the SQLite storage backend,
which is the backend examples/cloudflare/wrangler.toml declares through
new_sqlite_classes. R2 has a free tier of its own. Check the current limits for
each against your traffic.
Wrangler is a dev dependency of this repository, so every command below uses
npx. Drop the prefix if you installed it globally.
Steps
-
Create the R2 bucket
npx wrangler r2 bucket create pds-blobs -
Set the secrets
npx wrangler secret put PDS_PASSWORD npx wrangler secret put JWT_SECRET -
Deploy
cd examples/cloudflare npx wrangler deploy -
Register the DID
npm run setup -- --pds https://your-worker.workers.dev
Permissioned data
Uncomment the [vars] block in examples/cloudflare/wrangler.toml before you
deploy. See permissioned data.
Your own Worker
The package exports the fetch handler and the Durable Object class. Point
wrangler.toml at @pdsjs/cloudflare, or re-export from your own entrypoint:
export { default, PDSDurableObject } from '@pdsjs/cloudflare'
Your wrangler.toml needs the same bindings as
examples/cloudflare/wrangler.toml:
| Binding | Kind | Holds |
|---|---|---|
PDS | Durable Object, PDSDurableObject | The repo and the account data, in Durable Object SQLite |
BLOBS | R2 bucket | Blobs |
BACKUPS | R2 bucket, optional | Scheduled backups |
EMAIL | Send Email, optional | Account verification mail |
The Durable Object needs the new_sqlite_classes migration. Keep BACKUPS in a
separate bucket from BLOBS. The retention policy deletes freely inside it.
Account verification mail (confirming an address, authorizing a PLC operation)
goes out through the Email Service EMAIL binding:
[[send_email]]
name = "EMAIL"
The From address must use a domain onboarded for Email Sending:
npx wrangler email sending enable example.com
Set PDS_EMAIL_FROM to the sender, or leave it out to default to
no-reply@HOSTNAME. A workers.dev hostname cannot be onboarded, so a custom
domain is required. With the binding and a sender in place the account page
offers "Email for recovery"; without them it stores an address that stays
unconfirmed. See configuration.
Static sites
Set PDS_EXPERIMENTAL_SITE_DOMAIN and route the domain's traffic to the Worker. Two zone
routes cover every site, present and future:
[[routes]]
pattern = "example.com/*"
zone_name = "example.com"
[[routes]]
pattern = "*.example.com/*"
zone_name = "example.com"
DNS needs two proxied records in the zone: one for the apex and a wildcard
(*, CNAME to the apex works). With both in place, deploying a site record
is the whole release: blog is live at blog.example.com with no further
configuration. www with no www site deployed redirects to the apex.
- Routes only intercept proxied traffic. A DNS-only (grey cloud) record bypasses the Worker silently: the route deploys cleanly, and requests keep going to whatever the record points at. Check the proxy status first when a routed hostname serves the wrong thing.
- A hostname that must not reach the Worker (a legacy host on the same zone) stays out by keeping its record DNS-only.
- Per-site
custom_domainentries also work, but wrangler only ever attaches them: removing one fromwrangler.tomland redeploying does not detach it. Delete them in the dashboard or over the API. - Two zone features rewrite site HTML in flight: HTML post-processing strips
the ETag, and Browser Cache TTL raises
max-age, so a redeploy can look stuck for hours. Set Browser Cache TTL to "Respect Existing Headers", or scope a Configuration Rule to the site hostnames. - With the zone's image transformations enabled ("Transform via URL"), the
/.blobs/<cid>route composes with/cdn-cgi/image/for resized variants of repo images. The blobs are immutable, so transformed variants cache indefinitely.
Operating notes
The Durable Object holds the subscribeRepos WebSockets. A hard restart of the
object stops federation, and it stays stopped until those sockets close. Close
them gracefully instead.