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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
187 lines · 8.8 KB Raw History

Configuration

createServer takes an options object. The example servers map environment variables onto those options, and this page documents that mapping. The Node example is examples/node/index.js. The container entrypoint is docker/server.js. A deployment that imports @pdsjs/node as a library passes the options directly and reads no environment at all.

Required

VariableDescription
PDS_PASSWORDPassword for legacy auth and OAuth consent
JWT_SECRETSecret that signs JWTs
HOSTNAMEPublic hostname of the PDS. The container reads PDS_HOSTNAME

The container exits when any of the three is absent. The Node example starts without them, which suits local development and no deployment.

None of the three has a safe default. A PDS that generates its own signing secret issues tokens anyone can forge. A PDS with no account password accepts every password. A PDS that guesses its hostname serves a DID document nobody resolves.

Generate the signing secret once and keep it:

openssl rand -hex 32 > jwt-secret

A fresh secret on every restart signs out every user.

Server

VariableDefaultDescription
PORT3000Server port
PDS_DB_PATH./pds.dbSQLite database path. The container uses /data/pds.db
PDS_BLOBS_DIR./blobsBlob storage directory. The container uses /data/blobs
PDS_ACCOUNT_UItrueSet to false to serve no account interface

PDS_DB_PATH and PDS_BLOBS_DIR apply to the Node and Deno servers. The Cloudflare Worker takes its storage from bindings instead.

Federation

VariableDefaultDescription
PLC_URLhttps://plc.directoryPLC directory URL
RELAY_URLnoneRelay that receives requestCrawl
APPVIEW_URLnoneAppView that receives proxied app.bsky.* requests
APPVIEW_DIDnoneAppView DID. Service auth tokens name it as the audience

Set both AppView variables or neither. The PDS signs a service auth token for APPVIEW_DID and sends it to APPVIEW_URL.

Mail

VariableDefaultDescription
PDS_EMAIL_TRANSPORTnonesmtp sends mail to an SMTP host. log prints it to the terminal. Applies to the Node and Deno servers
PDS_SMTP_HOSTnoneSMTP host, for the smtp transport
PDS_SMTP_PORTnoneSMTP port, for the smtp transport
PDS_EMAIL_FROMnoneSender address. For the Node/Deno smtp transport, an override that defaults to no-reply@localhost. On Cloudflare, the sender mail is sent as; absent, no-reply@HOSTNAME is used

Without a sender the PDS stores an address and confirms nothing. It then issues no confirmation code, and a session stays the only authorization for a PLC identity operation.

The docker compose stack runs Mailpit, so PDS_EMAIL_TRANSPORT=smtp in local development shows sent mail in a browser.

The Cloudflare Worker has no transport switch: the EMAIL binding is the sender. Mail is enabled only when the binding is present and a sender resolves — PDS_EMAIL_FROM, else no-reply@HOSTNAME. The From address's domain must be onboarded for Email Sending. See Deploy: Cloudflare Workers.

Backups

VariableDefaultDescription
PDS_BACKUP_DIRnoneDirectory that receives scheduled backups
PDS_BACKUP_S3_BUCKETnoneS3-compatible bucket that receives scheduled backups
PDS_BACKUP_S3_ENDPOINTnoneBucket endpoint
PDS_BACKUP_S3_REGIONus-east-1Bucket region, where the provider needs one
PDS_BACKUP_S3_ACCESS_KEYnoneBucket access key
PDS_BACKUP_S3_SECRET_KEYnoneBucket secret key

A directory is the simple case. Point it at a different disk than the database. The S3 path needs no SDK, and it works with Bunny, Backblaze B2, MinIO, R2, and AWS. With neither set, the backups page reports backups as unavailable.

Features

Boolean switches are named PDS_ENABLE_<FEATURE>; variables that carry a value are named PDS_<FEATURE>_<PROPERTY>.

VariableDefaultDescription
PDS_ENABLE_SPACESfalseEnable permissioned data

Experimental features

Experiments are exempt from semver: any PDS_EXPERIMENTAL_* variable, and anything under the server adapters' experimental option, may change shape or disappear in any release, including patches. A feature graduates by moving to the stable names above, keeping the experimental names as aliases for one more release.

VariableDefaultDescription
PDS_EXPERIMENTAL_QUERIESfalseServe dev.pdsjs.query.run over this repo's dev.pdsjs.query.def records. A definition names a live view over one collection, with equality filters and one sort field; the run endpoint returns the whole view as JSON in one request. Queries read the public repo only
PDS_EXPERIMENTAL_GIT_HTTPfalseServe hosted git repositories to stock git over read-only smart HTTP under /git/. See the git package
PDS_EXPERIMENTAL_GIT_BROWSEtrueFile browsing of hosted git repositories on the account page. Set to false to turn it off
PDS_EXPERIMENTAL_SITE_DOMAINnoneApex domain static sites serve under. Setting it enables the feature
PDS_EXPERIMENTAL_SITE_APEX_NAMEhomeSite served at the apex of the site domain

Git hosting outside the PDS

Git hosting does not have to live in the PDS. The git-host package is a separate service that reads the account's records and blobs over the public XRPC surface, serves git smart HTTP, the file browser, the dev.pdsjs.git.* methods and the browser app from its own origin, and indexes collaboration records off Jetstream. The PDS stays the data store and the identity provider; pushes go to the PDS exactly as before.

The Node adapter moves the reads out with experimental: { git: { external: true } }. Two halves of the enforcement question deserve their own names:

  • external: true stops the PDS serving git entirely. Branch protection loses its enforcement point, because the only place a write veto means anything is the server that holds the record, and that is no longer a git server.
  • guardOnly: true drops the routes and keeps the write guard. Protected branches still refuse force pushes and deletion at the record write, and the external forge can say what it likes. external implies guardOnly.

On Cloudflare the embedded path is already opt-in through the PDS_EXPERIMENTAL_GIT_* variables; an external forge needs nothing of the PDS but a running one.

Static sites

Sites are off without a site domain, and the Node and Deno servers take one as the experimental: { sites: { domain } } option instead. A site's record key is a DNS label and that label is the subdomain it answers on, so blog serves at blog.<site domain>.

The site domain has to differ from HOSTNAME. The PDS's own hostname is checked first and never reaches the site handler, so a site domain equal to it serves the PDS and no site. Keeping them apart is also what keeps repo-controlled HTML off the origin holding the account session cookie.

The hostnames have to reach the server. On Cloudflare that means proxied DNS records and Worker routes; one wildcard of each covers every site so a new deploy needs no configuration. Routes only intercept proxied traffic, so a DNS-only record bypasses the Worker silently. Universal SSL covers a single wildcard level, so *.example.com needs no extra certificate but *.sites.example.com does. deploy-cloudflare.md has the full route and DNS shape.

www with no site named www deployed redirects to the apex, path and query kept.

Sites serve over HTTPS only. Responses carry a year of HSTS, and on Cloudflare a plain-HTTP request is redirected before it reaches the server, because a custom domain answers on both schemes.

Every servable site hostname also answers /.blobs/<cid> with that blob from the repo, immutable and sandboxed, so a client-rendered site can show repo media without a manifest entry per file. The same bytes are already public at com.atproto.sync.getBlob.

Files serve with Cache-Control: public, max-age=60, must-revalidate unless the deploy set a per-file value (pdsjs-site deploy --cache-control). Content is addressed by hash, so fingerprinted assets can safely take public, max-age=31536000, immutable. On Cloudflare, site responses are also held in the edge cache for their max-age, so repeat requests are answered at the edge instead of crossing to the Durable Object's region.

Example file

.env.example in the repository root holds a template for the Node server.

cp .env.example .env