···11+# Tranquil PDS Configuration Reference
22+33+Every option in `example.toml` is overridable by an environment variable. This document focuses on what each setting *does* and why some of them are critical to get right before you start. The install guides (`install-containers.md`, `install-kubernetes.md`, `install-nix.md`) cover the mechanics of standing up the service; this covers what to put in it.
44+55+## Configuration methods
66+77+The server accepts config in two forms, applied in this order (later wins):
88+99+1. A TOML file, defaulting to `/etc/tranquil-pds/config.toml`. Override the path with `TRANQUIL_PDS_CONFIG`.
1010+2. Environment variables. Every TOML key has a corresponding env var documented in `example.toml`.
1111+1212+In practice: TOML for static, non-sensitive settings; env vars (injected from secrets) for anything that would be unfortunate in a file you may want to commit.
1313+1414+---
1515+1616+## Required settings
1717+1818+These have no defaults and the Tranqil will not start without them.
1919+2020+### `PDS_HOSTNAME`
2121+2222+The public hostname of your PDS. Example: `pds.example.com`.
2323+2424+This is used in DID documents, OAuth metadata, and the `/.well-known/atproto-did` endpoint. Getting it wrong after you have accounts is painful: DIDs are registered against this hostname in the PLC directory, and changing it requires a PLC rotation operation. Set it correctly before creating any accounts.
2525+2626+If your PDS hostname is also the apex domain (e.g. `example.com` rather than `pds.example.com`), users can have their handle *be* the apex domain (e.g. `@example.com`) since the PDS serves `/.well-known/atproto-did` directly without needing a DNS TXT record.
2727+2828+### `DATABASE_URL`
2929+3030+PostgreSQL connection string. Example: `postgres://tranquil_pds:password@host:5432/pds`
3131+3232+Tranquil PDS uses Postgres for all persistent state: accounts, repos, sessions, the comms queue, and (when using the default `postgres` repo backend) the full block store for every account's atproto repository.
3333+3434+---
3535+3636+## Required secret/sensitive env vars
3737+3838+These three are the most sensitive values in the entire deployment. They should never appear in a committed file, in any circumstance.
3939+4040+### `JWT_SECRET`
4141+4242+Used to sign and verify all session JWTs issued by the PDS.
4343+4444+Generate: `openssl rand -base64 48`
4545+4646+### `DPOP_SECRET`
4747+4848+Used to validate DPoP (Demonstrating Proof of Possession) proofs in OAuth flows. DPoP is the binding mechanism that ties an OAuth access token to the client's key pair, preventing token theft from replaying the token with a different client.
4949+5050+Generate: `openssl rand -base64 48`
5151+5252+### `MASTER_KEY`
5353+5454+Used as input to HKDF key derivation and for encrypting per-account signing keys at rest. Every account's atproto repo signing key is derived from or wrapped with this value.
5555+5656+Generate: `openssl rand -base64 48`
5757+5858+---
5959+6060+## Handle domains
6161+6262+### `PDS_USER_HANDLE_DOMAINS`
6363+6464+Comma-separated list of domains under which handles are issued. Defaults to `PDS_HOSTNAME` if unset.
6565+6666+If your PDS hostname is `pds.example.com` but you want handles like `alice.example.com`, set this to `example.com`. You need a wildcard TLS cert and DNS wildcard record for the domain(s) you list here.
6767+6868+### `PDS_BANNED_WORDS`
6969+7070+Comma-separated list of substring patterns blocked at registration time. Useful to prevent handles that would collide with your infrastructure (e.g. `grafana`, `vault`, `relay`). The server has a built-in exact-match reserved list (`admin`, `api`, `git`, `mail`, `ssh`, etc.); this adds substring matches for anything missing from that list.
7171+7272+### `INVITE_CODE_REQUIRED`
7373+7474+Default `true`. Keep this enabled on any public-facing PDS unless you specifically want open registration. Invite codes are generated via the admin API/UI.
7575+7676+---
7777+7878+## Blob storage
7979+8080+### `BLOB_STORAGE_BACKEND`
8181+8282+`filesystem` (default) or `s3`. Pretty straight forward, and dependent on your personal infrastructure requirements and availability.
8383+8484+### `BLOB_STORAGE_PATH`
8585+8686+Path on disk for the filesystem backend. Default `/var/lib/tranquil-pds/blobs`.
8787+8888+For S3: set `S3_BUCKET` and optionally `S3_ENDPOINT` for S3-compatible stores (Tigris, Cloudflare R2, MinIO, etc.).
8989+9090+---
9191+9292+## Federation
9393+9494+### `CRAWLERS`
9595+9696+Comma-separated list of relay URLs to notify when new events are committed to an account's repo. Often `https://bsky.network` for production, this is what allows your PDS to be federated to on the wider network.
9797+9898+---
9999+100100+## Email
101101+102102+Email is sorta optional but required for account verification, password resets, and 2FA backup codes. If `MAIL_FROM_ADDRESS` is not set, email sending is disabled entirely.
103103+104104+Tranquil supports two delivery modes:
105105+106106+### Smarthost (SMTP relay)
107107+108108+Set `MAIL_SMARTHOST_HOST`, `MAIL_SMARTHOST_PORT`, `MAIL_SMARTHOST_USERNAME`, and `MAIL_SMARTHOST_PASSWORD`. Use this if you're routing through a sending service (Postmark, SES, etc.) or a local MTA.
109109+110110+TLS mode defaults to `starttls`. Setting `tls = "none"` alongside a password is rejected at startup.
111111+112112+### DKIM signing (`email.dkim`)
113113+114114+Set `MAIL_DKIM_SELECTOR`, `MAIL_DKIM_DOMAIN`, and `MAIL_DKIM_KEY_PATH` to sign outgoing mail. The key file should be PEM-format RSA or Ed25519. The corresponding DNS TXT record at `<selector>._domainkey.<domain>` must be published before mail is sent.
115115+116116+---
117117+118118+## Notifications
119119+120120+Beyond email, Tranquil supports Discord, Telegram, and Signal for user notifications. Each is opt-in:
121121+122122+- **Discord**: Set `DISCORD_BOT_TOKEN`.
123123+- **Telegram**: Set `TELEGRAM_BOT_TOKEN` and `TELEGRAM_WEBHOOK_SECRET`.
124124+- **Signal**: Set `SIGNAL_ENABLED=true` after linking a device via the admin API. State is persisted in `signal_*` tables in Postgres.
125125+126126+---
127127+128128+## SSO
129129+130130+OAuth SSO is supported for GitHub, Discord, Google, GitLab, generic OIDC, and Apple. Each provider requires `enabled = true`, a `client_id`, and a `client_secret`. GitLab and OIDC also need an `issuer` URL.
131131+132132+`client_secret` values should be injected as env vars (`SSO_GITHUB_CLIENT_SECRET`, etc.) and not placed in config files.
133133+134134+---
135135+136136+## Cache backend
137137+138138+### `CACHE_BACKEND`
139139+140140+`ripple` (default) or `valkey`. Ripple is the a built-in in-process gossip cache. Valkey is an external Redis fork with it's own complications.
141141+142142+---
143143+144144+## Optional keys worth knowing
145145+146146+| Env var | Default | Notes |
147147+|---|---|---|
148148+| `PLC_ROTATION_KEY` | — | Operator-level PLC rotation key (DID key). If unset, per-user keys are used. |
149149+| `ENABLE_PDS_HOSTED_DID_WEB` | `false` | Opt-in did:web hosting. Long-term commitment — only enable if you intend to serve DID documents indefinitely. |
150150+| `MAX_BLOB_SIZE` | 10 GiB | Per-blob upload limit in bytes. |
151151+| `DISABLE_RATE_LIMITING` | `false` | Only for testing. Avoid in production. |
152152+| `DATABASE_MAX_CONNECTIONS` | 100 | Connection pool ceiling. Tune against your Postgres `max_connections`. |
153153+| `FIREHOSE_BACKFILL_HOURS` | 72 | How many hours of history relays can replay from cursor. |
154154+| `REPO_BACKEND` | `postgres` | `tranquil-store` is the experimental embedded backend. Careful if attempting to run in production. |
+209-14
docs/install-kubernetes.md
···11-# Tranquil PDS on kubernetes
11+# Tranquil PDS on Kubernetes
2233-If you're reaching for kubernetes for this app, you're experienced enough to know how to spin up:
33+If you're reaching for Kubernetes for this app, you're experienced enough to know how to spin up:
4455- cloudnativepg (or your preferred postgres operator)
66- a PersistentVolume for blob storage
77- the app itself (it's just a container with some env vars)
8899-You'll need a wildcard TLS certificate for `*.your-pds-hostname.example.com`. User handles are served as subdomains.
99+See [configuration.md](configuration.md) for what each env var does and why the secret ones matter. This guide covers the Kubernetes-specific wiring.
1010+1111+---
1212+1313+## TLS and DNS
1414+1515+You need a wildcard TLS certificate covering `*.your-pds-hostname.example.com` — user handles resolve as subdomains, so every user's handle requires a matching cert SAN.
1616+1717+An approach using Cert Manager would look something like this:
1818+1919+```yaml
2020+apiVersion: cert-manager.io/v1
2121+kind: Certificate
2222+metadata:
2323+ name: pds-cert
2424+ namespace: pds
2525+spec:
2626+ secretName: pds-tls
2727+ dnsNames:
2828+ - pds.example.com
2929+ - "*.pds.example.com"
3030+```
3131+3232+If your PDS hostname is the apex domain (so handles are issued under it, not under a subdomain), include the apex in `dnsNames` alongside the wildcard.
3333+3434+---
3535+3636+## Secrets
3737+3838+The three secrets primary key secrets (`JWT_SECRET`, `DPOP_SECRET`, `MASTER_KEY`) must never appear in a manifest or config file. Inject them as a Kubernetes Secret, sourced from wherever you manage secrets.
3939+4040+To create the Secret directly and manage rotation manually:
4141+4242+```bash
4343+kubectl create secret generic pds-secrets -n pds \
4444+ --from-literal=DATABASE_URL='postgres://...' \
4545+ --from-literal=JWT_SECRET="$(openssl rand -base64 48)" \
4646+ --from-literal=DPOP_SECRET="$(openssl rand -base64 48)" \
4747+ --from-literal=MASTER_KEY="$(openssl rand -base64 48)"
4848+```
4949+5050+Then reference it in your Deployment:
5151+5252+```yaml
5353+envFrom:
5454+ - secretRef:
5555+ name: pds-secrets
5656+```
5757+5858+---
5959+6060+## PostgreSQL
6161+6262+CloudNativePG is an easy recommendation, an example for the purpose of Tranquil PDS:
6363+6464+```yaml
6565+apiVersion: postgresql.cnpg.io/v1
6666+kind: Cluster
6767+metadata:
6868+ name: postgres
6969+ namespace: pds
7070+spec:
7171+ instances: 1
7272+ bootstrap:
7373+ initdb:
7474+ database: pds
7575+ owner: tranquil_pds
7676+ secret:
7777+ name: postgres-user-secret # k8s Secret with username/password
7878+ storage:
7979+ storageClass: your-storage-class
8080+ size: 10Gi
8181+```
8282+8383+The `postgres-user-secret` Secret needs `username` and `password` keys. The password you put here is what goes into `DATABASE_URL`.
8484+8585+Any standard Postgres setup works in place of CNPG. Tranquil does not require anything special for a basic installation.
8686+8787+---
8888+8989+## Storage PVC (if using local-path, nfs, or similar)
9090+9191+```yaml
9292+apiVersion: v1
9393+kind: PersistentVolumeClaim
9494+metadata:
9595+ name: pds-blobs
9696+ namespace: pds
9797+spec:
9898+ storageClassName: your-storage-class
9999+ accessModes:
100100+ - ReadWriteOnce
101101+ resources:
102102+ requests:
103103+ storage: 50Gi
104104+```
105105+106106+Full Deployment example:
107107+108108+```yaml
109109+apiVersion: apps/v1
110110+kind: Deployment
111111+metadata:
112112+ name: pds
113113+ namespace: pds
114114+spec:
115115+ replicas: 1
116116+ selector:
117117+ matchLabels:
118118+ app: pds
119119+ strategy:
120120+ type: Recreate
121121+ template:
122122+ metadata:
123123+ labels:
124124+ app: pds
125125+ spec:
126126+ containers:
127127+ - name: pds
128128+ image: atcr.io/tranquil.farm/tranquil-pds:latest
129129+ ports:
130130+ - name: http
131131+ containerPort: 3000
132132+ env:
133133+ - name: SERVER_HOST
134134+ value: "0.0.0.0"
135135+ - name: SERVER_PORT
136136+ value: "3000"
137137+ - name: PDS_HOSTNAME
138138+ value: "pds.example.com"
139139+ - name: PDS_USER_HANDLE_DOMAINS
140140+ value: "example.com"
141141+ - name: BLOB_STORAGE_BACKEND
142142+ value: filesystem
143143+ - name: BLOB_STORAGE_PATH
144144+ value: /var/lib/tranquil/blobs
145145+ - name: CRAWLERS
146146+ value: "https://bsky.network"
147147+ - name: INVITE_CODE_REQUIRED
148148+ value: "true"
149149+ envFrom:
150150+ - secretRef:
151151+ name: pds-secrets
152152+ volumeMounts:
153153+ - mountPath: /var/lib/tranquil/blobs
154154+ name: blobs
155155+ readinessProbe:
156156+ httpGet:
157157+ path: /xrpc/_health
158158+ port: http
159159+ initialDelaySeconds: 15
160160+ periodSeconds: 10
161161+ failureThreshold: 6
162162+ resources:
163163+ requests:
164164+ memory: 256Mi
165165+ cpu: 100m
166166+ limits:
167167+ memory: 1Gi
168168+ volumes:
169169+ - name: blobs
170170+ persistentVolumeClaim:
171171+ claimName: pds-blobs
172172+```
101731111-The container image expects:
1212-- A TOML config file mounted at `/etc/tranquil-pds/config.toml` (or passed via `--config`)
1313-- `DATABASE_URL` - postgres connection string
1414-- `BLOB_STORAGE_PATH` - path to blob storage (mount a PV here)
1515-- `PDS_HOSTNAME` - your PDS hostname (without protocol)
1616-- `JWT_SECRET`, `DPOP_SECRET`, `MASTER_KEY` - generate with `openssl rand -base64 48`
1717-- `CRAWLERS` - typically `https://bsky.network`
174174+`SERVER_HOST: "0.0.0.0"` is required — the default `127.0.0.1` isn't reachable by either the Kubelet for health checks or your ingress controller.
175175+176176+---
177177+178178+## Ingress
181791919-and more, check the example.toml for all options. Environment variables can override any TOML value.
2020-You can also point to a config file via the `TRANQUIL_PDS_CONFIG` env var.
180180+The ingress rule must match both the PDS hostname itself and the wildcard for user handles.
181181+182182+```yaml
183183+apiVersion: networking.k8s.io/v1
184184+kind: Ingress
185185+metadata:
186186+ name: pds
187187+ namespace: pds
188188+spec:
189189+ tls:
190190+ - hosts:
191191+ - pds.example.com
192192+ - "*.example.com"
193193+ secretName: pds-tls
194194+ rules:
195195+ - host: pds.example.com
196196+ http:
197197+ paths:
198198+ - path: /
199199+ pathType: Prefix
200200+ backend:
201201+ service:
202202+ name: pds
203203+ port:
204204+ number: 3000
205205+ - host: "*.example.com"
206206+ http:
207207+ paths:
208208+ - path: /
209209+ pathType: Prefix
210210+ backend:
211211+ service:
212212+ name: pds
213213+ port:
214214+ number: 3000
215215+```
212162222-Health check: `GET /xrpc/_health`
217217+---
2321824219## Custom homepage
252202626-Mount a ConfigMap with your `homepage.html` into the container's frontend directory and it becomes your landing page. Go nuts with it. Account dashboard is at `/app/` so you won't break anything.
221221+Mount a ConfigMap with your `homepage.html` into the container's frontend directory and it becomes your landing page. The account dashboard lives at `/app/` so you won't displace it.
2722228223```yaml
29224apiVersion: v1