- Erlang 100%
| apps/pds | ||
| config | ||
| .codex | ||
| .elp.toml | ||
| .gitignore | ||
| LICENSE.md | ||
| README.md | ||
| rebar.config | ||
| rebar.lock | ||
pds
An Erlang/OTP ATProto Personal Data Server built on Cowboy.
This repository implements the core PDS runtime shape:
- Cowboy XRPC routing under
/xrpc/:nsid - local Erlang storage for accounts, sessions, records, blobs, and repo events
- ATProto account/session/app-password endpoints with
did:plccreation through the PLC directory - repo record mutation, lookup, listing, blob upload, and CAR export
- identity handle/DID helpers, verified
updateHandle, localresolveDid/resolveIdentity, hosted/.well-known/atproto-did, and hosted/.well-known/did.json - OAuth metadata, PAR, authorization-code, refresh-token, and JWKS endpoints with stored single-use grants and PKCE checks
- sync list/status/latest/repo/record/blob endpoints plus
subscribeReposWebSocket frames - official vendored
com.atproto.*andapp.bsky.*lexicons app.bsky.*and validatedatproto-proxyforwarding, includingdid:webanddid:plcDID document service resolution- per-account secp256k1 signing keys for DID docs and ES256K service auth JWTs
- PBKDF2-SHA256 password hashing with legacy SHA256 verification for existing local data
- sharded local persistence for accounts, sessions, records, blobs, repo status, OAuth grants, and events
- EUnit coverage for validators, JWTs, codecs, OAuth grants, storage state, HTTP routing, lexicons, proxy resolution, PLC operations, stale swaps, missing blobs, firehose framing, and blob roundtrips
Build
rebar3 compile
rebar3 eunit
For an OTP smoke test without opening a listener, use http.enabled => false:
erl -pa _build/default/lib/*/ebin -noshell \
-eval 'application:load(pds),
application:set_env(pds, storage, #{data_dir => "/tmp/pds-smoke", persist => false}),
application:set_env(pds, http, #{enabled => false}),
io:format("~p~n", [application:ensure_all_started(pds)]),
application:stop(pds),
halt().'
For normal local use, keep HTTP enabled and run:
rebar3 shell
The default listener is 0.0.0.0:8080.
Configuration
Runtime configuration lives in config/sys.config.
http: listener IP/port, body limits, blob limits, andenabledstorage: local data directory and persistence toggleidentity: public base URL, hosted handle domain, optionalavailable_user_domains, PLC directory andplc_submit, default AppView, explicitservice_proxies, optionalhandle_resolutionstest/operator overrides, and optional DID document overridesauth: issuer, token TTLs, OAuth grant TTLs, PBKDF2 iteration count, local session JWT secret, and admin passwordlimits: repo pagination and firehose buffer knobs
apps/pds/priv/data/ is ignored and is the default local persistence directory.
Compliance Notes
The implementation follows ATProto wire shapes and endpoint names, but it is not
yet a production-certified Bluesky PDS. Service auth JWTs are asymmetrically
signed with account keys published in DID documents, PLC creation/update uses
signed plc_operation objects submitted to the configured directory, OAuth
authorization-code issuance is stored and single-use, blob CIDs use the raw
multicodec, and repo CAR exports include deterministic commit/tree/record blocks.
Remaining broad interop work includes the PLC email-token flow, OAuth DPoP proof
verification, a complete signed MST repository implementation, deeper recursive
Lexicon validation, full firehose backfill/diff fidelity, and tests against the
official TypeScript implementation.