atoll PDS

Getting started

Setup environment

Atoll is configured through environment variables. This page groups them by what they do, from the four secrets that gate boot to the optional subsystems. The production deployment runbook walks the same ground in order; use this page when you need to look one value up.

Store the environment in a root-owned file such as /etc/atoll/atoll.env with mode 0600.

Generate the secrets#

mix phx.gen.secret            # SECRET_KEY_BASE
openssl rand -base64 32       # ATOLL_KEY_ENCRYPTION_KEY
openssl rand -base64 32       # ATOLL_SESSION_SIGNING_KEY
openssl rand -base64 32       # ATOLL_OAUTH_NONCE_SECRET
openssl rand -base64 32       # ATOLL_PDS_SIGNING_KEY (secp256k1 service key)

Also choose a strong ATOLL_ADMIN_PASSWORD for the operator endpoints.

Put ATOLL_KEY_ENCRYPTION_KEY and ATOLL_PDS_SIGNING_KEY in a durable secret store, with an offline copy, before anything else. The key-encryption key wraps every account's repository and PLC signing key: if it is lost, custody in the database is unrecoverable and every hosted identity is stranded. Back it up with the same care as the database, and keep old keys listed in ATOLL_PREVIOUS_KEY_ENCRYPTION_KEYS until a rewrap completes — see Key custody and recovery.

Required to boot#

Production refuses to start without these, so misconfiguration fails at startup rather than on first use.

PHX_SERVER=true
PORT=4000
DATABASE_URL=ecto://atoll:PASSWORD@localhost/atoll_prod
SECRET_KEY_BASE=...
ATOLL_KEY_ENCRYPTION_KEY=...
ATOLL_SESSION_SIGNING_KEY=...
PHX_HOST=pds.example.com
ATOLL_PDS_DID=did:web:pds.example.com
ATOLL_AVAILABLE_USER_DOMAINS=.users.example.com

PHX_HOST must be a DNS hostname with no scheme, port, or path; it sets Phoenix's public HTTPS URL on port 443. ATOLL_AVAILABLE_USER_DOMAINS is a comma-separated list of dot-prefixed suffixes such as .example.com,.example.org; values are lowercased and deduplicated, and an empty value clears the list. Production advertises no handle domains unless this is set.

The server DID is the session JWT audience, so changing ATOLL_PDS_DID invalidates existing session tokens.

Required in practice#

A server boots without these but is not a working PDS.

ATOLL_PDS_SIGNING_KEY=...                 # signs /.well-known/did.json service identity
ATOLL_OAUTH_NONCE_SECRET=...              # OAuth (modern clients) is unusable without it
ATOLL_ADMIN_PASSWORD=...                  # operator/admin endpoints
ATOLL_TRUSTED_PROXY_CIDRS=127.0.0.1/32    # your reverse proxy's address(es)
ATOLL_LISTEN_IP=127.0.0.1                 # bind to loopback when the proxy shares the host

Federation#

ATOLL_APPVIEW_PROXY=did:web:api.bsky.app#bsky_appview
ATOLL_RELAY_URLS=https://bsky.network
ATOLL_RELAY_CRAWL_ENABLED=true
ATOLL_MOD_SERVICE_PROXY=did:plc:ar7c4by46qjdydhdevvrndac#atproto_labeler
ATOLL_REPORT_SERVICE_PROXY=did:plc:ar7c4by46qjdydhdevvrndac#atproto_labeler
ATOLL_IMAGE_CDN_URL_PATTERN=https://cdn.bsky.app/img/%s/plain/%s/%s@jpeg

Verify the moderation-service DIDs yourself before relying on them; service DIDs are operator configuration, not protocol constants. See Operations for relays and proxying.

Signup policy#

ATOLL_SIGNUP_ENABLED=false
ATOLL_INVITE_CODE_REQUIRED=true

Leave signup off until the server is otherwise verified. Self-service signup refuses operational first labels (www, admin, mail, pds, cdn, …) beneath the handle domains; tune the list with ATOLL_RESERVED_HANDLES. See Accounts.

ATOLL_METRICS_ENABLED=true
ATOLL_BLOB_CLEANUP_ENABLED=true
ATOLL_ACCOUNT_CLEANUP_ENABLED=true
ATOLL_IDENTITY_REFRESH_ENABLED=true
ATOLL_EVENT_RETENTION_ENABLED=true
ATOLL_EVENT_RETENTION_SECONDS=...
ATOLL_OAUTH_KEY_CHECKS_ENABLED=true
ATOLL_MEDIA_VALIDATION=images

Pair metrics with the alert rules and runbook in ops/prometheus.

Database#

DATABASE_URL=ecto://atoll:PASSWORD@host/atoll_prod?ssl=true
POOL_SIZE=10
ECTO_IPV6=true

Optional read replica#

Leave READ_DATABASE_URL unset to use the primary for all database access. To enable a separate read pool:

READ_DATABASE_URL=ecto://atoll_reader:PASSWORD@replica.example.com/atoll_prod?ssl=true
READ_POOL_SIZE=10

READ_POOL_SIZE defaults to POOL_SIZE, or 10 when neither is set. The reader uses the same ECTO_IPV6 setting as the primary. Provision replication separately; Atoll does not create or manage PostgreSQL replicas. READ_DATABASE_URL is rejected with SQLite. The replica semantics — what stays on the primary, and where replication lag is visible — are documented in the deployment runbook.

SQLite#

ATOLL_DATABASE=sqlite
DATABASE_PATH=/absolute/path/to/atoll.sqlite3

ATOLL_DATABASE is read at build time as well as at runtime. See SQLite.

Optional subsystems#

Subsystem Variables
S3 blob storage ATOLL_BLOB_STORAGE=s3 plus the ATOLL_S3_* settings
Redis-shared rate limits ATOLL_RATE_LIMIT_BACKEND=redis, ATOLL_REDIS_URL
Email worker ATOLL_EMAIL_WORKER_URL, ATOLL_EMAIL_WORKER_TOKEN
Passkeys ATOLL_PASSKEYS_ENABLED=true
Verified PLC resolution ATOLL_PLC_RESOLUTION_MODE=audit
Key rotation ATOLL_PREVIOUS_KEY_ENCRYPTION_KEYS
Policy links ATOLL_PRIVACY_POLICY_URL, ATOLL_TERMS_OF_SERVICE_URL, ATOLL_CONTACT_EMAIL

Without the email worker, email confirmation, password reset, and email-authorized PLC signing cannot send; a ready-to-deploy Cloudflare Worker lives in ops/email-worker. Quotas, cache TTLs, and rate limits are covered in Operations.

Development#

ATOLL_PDS_DID and ATOLL_AVAILABLE_USER_DOMAINS also apply in development; unset values preserve the development and test configuration under config :atoll, :pds. For a development PDS on port 4000:

ATOLL_LOCALHOST_DIDS_ENABLED=true
ATOLL_PDS_DID='did:web:localhost%3A4000'
ATOLL_PDS_SIGNING_KEY=...

ATOLL_LOCALHOST_DIDS_ENABLED works in development and test builds only; enabling it through runtime configuration in production fails startup. See Local development for what the exception does and does not allow.

Type to search across every page.