···11+# (optional) debug, info, warn, or error
22+# default: info
33+# PDS_LOG_LEVEL=info
44+55+# (optional) directory where databases and blobs will be stored
66+# default: ./data [relative to cwd]
77+# PDS_DATA_DIR=./data
88+99+# (required) hostname where this PDS is running
1010+# for handles to work, you should set A records for *.{PDS_HOSTNAME} and PDS_HOSTNAME to point to this server
1111+PDS_HOSTNAME=
1212+1313+# (optional) the PDS's DID
1414+# default: did:web:{PDS_HOSTNAME}
1515+# PDS_DID=
1616+1717+# (optional) whether an invite code is needed to create an account
1818+# default: true
1919+# PDS_INVITE_CODE_REQUIRED=true
2020+2121+# (required) the rotation key the PDS will use, in multibase format
2222+# tip: run the `gen-keys` binary to generate this (more info in readme)
2323+PDS_ROTATION_KEY=
2424+2525+# (required) the key used to sign JWKs
2626+# tip: run the `gen-keys` binary to generate this (more info in readme)
2727+PDS_JWK_MULTIBASE=
2828+2929+# (required) the PDS admin password
3030+# this can be used to access com.atproto.admin.* endpoints as well as the /admin dashboard
3131+PDS_ADMIN_PASSWORD=
3232+3333+# (optional) relays/crawlers to inform to subscribe to this PDS, comma-separated
3434+# default: https://bsky.network
3535+# PDS_CRAWLERS=https://bsky.network
3636+3737+# (optional [but recommended]): a secret token used to generate DPoP nonces; 32 bytes, base64url
3838+# tip: run the `gen-keys` binary to generate this (more info in readme)
3939+# PDS_DPOP_NONCE_SECRET=
4040+4141+# (optional) email config (see readme for details)
4242+# auth uri should look like smtp[s]://user:pass@host[:port]
4343+# sender should look like e@mail.com or Name <e@mail.com>
4444+# without setting these, emails will be logged to stdout
4545+# PDS_SMTP_STARTTLS=false
4646+# PDS_SMTP_AUTH_URI=
4747+# PDS_SMTP_SENDER=
4848+4949+# (optional) S3 config (see readme for details)
5050+# set these to back up your databases and/or blobs to S3(-compatible storge)
5151+# PDS_S3_BLOBS_ENABLED=false
5252+# PDS_S3_BACKUPS_ENABLED=false
5353+# PDS_S3_BACKUP_INTERVAL_S=3600
5454+# PDS_S3_ENDPOINT=
5555+# PDS_S3_REGION=
5656+# PDS_S3_BUCKET=
5757+# PDS_S3_ACCESS_KEY=
5858+# PDS_S3_SECRET_KEY=
5959+# PDS_S3_CDN_URL=
···18181919to generate some of the environment variables you'll need.
20202121-Head to [`docker-compose.yaml`](docker-compose.yaml) and fill in the environment variables marked as required. See [Environment](#environment) for further details on configuration.
2121+Copy [`.env.example`](.env.example) to `.env` and fill in the environment variables marked as required. See [Environment](#environment) for further details on configuration.
22222323After that, run
2424+2425```
2526docker compose up -d
2627```
2828+2729to start the PDS, then navigate to `https://{PDS_HOSTNAME}/admin` to log in with the admin password you specified and create an invite code or a new account on the PDS.
28302931## environment
30323131-Documentation for most environment variables can be found in [`docker-compose.yaml`](docker-compose.yaml). There are two optional categories of environment variables that add functionality:
3333+Documentation for most environment variables can be found in [`.env.example`](.env.example). There are two optional categories of environment variables that add functionality:
32343335### SMTP
3636+3437The PDS can email users for password changes, identity updates, and account deletion. If these environment variables are not set, emails will instead be logged to the process' stdout.
35383639- `PDS_SMTP_AUTH_URI` — The URI to connect to the mail server. This should look like `smtp[s]://user:pass@host[:port]`.
···3841- `PDS_SMTP_SENDER` — The identity to send emails as. Can be an email address (`e@mail.com`) or a mailbox (`Name <e@mail.com>`).
39424043### S3
4444+4145The PDS can be configured to back up server data to and/or store blobs in S3(-compatible storage).
42464347- `PDS_S3_BLOBS_ENABLED=false` — Whether to store blobs in S3. By default, blobs are stored locally in `{PDS_DATA_DIR}/blobs/[did]/`.
4444-- `PDS_S3_BACKUPS_ENABLED=false` — Whether to back up data to S3.
4848+- `PDS_S3_BACKUPS_ENABLED=false` — Whether to back up data to S3.
4549- `PDS_S3_BACKUP_INTERVAL_S=3600` — How often to back up to S3, in seconds.
4650- `PDS_S3_ENDPOINT`, `PDS_S3_REGION`, `PDS_S3_BUCKET`, `PDS_S3_ACCESS_KEY`, `PDS_S3_SECRET_KEY` — S3 configuration.
4751- `PDS_S3_CDN_URL` — You may optionally set this to redirect `getBlob` requests to `{PDS_S3_CDN_URL}/blobs/{did}/{cid}`. When unset, blobs will be fetched either from local storage or from S3, depending on `PDS_S3_BLOBS_ENABLED`.
···50545155This repo contains several libraries in addition to the `pegasus` PDS:
52565353-| library | description |
5454-|----------|-------------|
5555-| frontend | The PDS frontend, containing the admin dashboard and account page. |
5757+| library | description |
5858+| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
5959+| frontend | The PDS frontend, containing the admin dashboard and account page. |
5660| ipld | A mostly [DASL-compliant](https://dasl.ing/) implementation of [CIDs](https://dasl.ing/cid.html), [CAR](https://dasl.ing/car.html), and [DAG-CBOR](https://dasl.ing/drisl.html). |
5757-| kleidos | An atproto-valid interface for secp256k1 and secp256r1 key management, signing/verifying, and encoding/decoding. |
5858-| mist | A [Merkle Search Tree](https://atproto.com/specs/repository#mst-structure) implementation for data repository purposes. |
5959-| pegasus | The PDS implementation. |
6161+| kleidos | An atproto-valid interface for secp256k1 and secp256r1 key management, signing/verifying, and encoding/decoding. |
6262+| mist | A [Merkle Search Tree](https://atproto.com/specs/repository#mst-structure) implementation for data repository purposes. |
6363+| pegasus | The PDS implementation. |
60646165To start developing, you'll need:
6666+6267- [`opam`](https://opam.ocaml.org/doc/Install.html), the OCaml Package Manager
6368- and the following packages, or their equivalents on your operating system: `cmake git libev-dev libffi-dev libgmp-dev libssl-dev libsqlite3-dev libpcre3-dev pkg-config`
64696570Start by creating an opam switch; similar to a Python virtual environment, storing the dependencies for this project and a specific compiler version. After that, install [`dune`](https://dune.build), the build system/package manager pegasus uses.
7171+6672```
6773opam switch create . 5.2.1
6874opam install dune
6975```
70767177You may need to run `eval $(opam env)` for this to work. Next, run
7878+7279```
7380dune pkg lock
7481```
8282+7583to solve dependencies.
76847777-Finally, after setting the required environment variables (see [Environment](#environment)), either run
8585+Set the required environment variables (see [Environment](#environment)), noting that the program won't automatically read from `.env`, then either run
8686+7887```
7988dune exec pegasus
8089```
9090+8191to run the program directly, or
9292+8293```
8394dune build
8495```
9696+8597to produce an executable that you'll likely find in `_build/default/bin`.
86988799For development, you'll also want to run
100100+88101```
89102dune tools exec ocamlformat
90103dune tools exec ocamllsp
91104```
105105+92106to download the formatter and LSP services. You can run `dune fmt` to format the project.
931079494-The [frontend](frontend) is written in [MLX](https://github.com/ocaml-mlx/mlx), a JSX-ish OCaml dialect. To format it, you'll need to `opam install ocamlformat-mlx`, then `ocamlformat-mlx -i frontend/**/*.mlx`. You'll see a few errors on formatting files containing `[%browser_only]`; I'm waiting on the next release of `mlx` to fix those.108108+The [frontend](frontend) is written in [MLX](https://github.com/ocaml-mlx/mlx), a JSX-ish OCaml dialect. To format it, you'll need to `opam install ocamlformat-mlx`, then `ocamlformat-mlx -i frontend/**/*.mlx`. You'll see a few errors on formatting files containing `[%browser_only]`; I'm waiting on the next release of `mlx` to fix those.
+23-59
docker-compose.yaml
···77 volumes:
88 - pds:/data
99 environment:
1010- # (optional) debug, info, warn, or error
1111- # default: info
1212- # - PDS_LOG_LEVEL=info
1313-1414- # (optional) directory where databases and blobs will be stored
1515- # default: ./data [relative to cwd]
1616- - PDS_DATA_DIR=./data
1717-1818- # (required) hostname where this PDS is running
1919- # for handles to work, you should configure *.hostname and hostname to point to this server
2020- # make sure to also set this env var for the caddy container below
2121- - PDS_HOSTNAME=
2222-2323- # (optional) the PDS's DID
2424- # default: did:web:{hostname}
2525- # - PDS_DID=
2626-2727- # (optional) whether an invite code is needed to create an account
2828- # default: true
2929- # - PDS_INVITE_CODE_REQUIRED=true
3030-3131- # (required) the rotation key the PDS will use, in multibase format
3232- # tip: run the `gen-keys` binary to generate this
3333- - PDS_ROTATION_KEY=
3434-3535- # (required) the key used to sign JWKs
3636- # tip: run the `gen-keys` binary to generate this
3737- - PDS_JWK_MULTIBASE=
3838-3939- # (required) the PDS admin password
4040- # this can be used to access com.atproto.admin.* endpoints as well as the /admin dashboard
4141- - PDS_ADMIN_PASSWORD=
4242-4343- # (optional) relays/crawlers to inform to subscribe to this PDS, comma-separated
4444- # default: https://bsky.network
4545- # - PDS_CRAWLERS=https://bsky.network
4646-4747- # (optional [but recommended]): a secret token used to generate DPoP nonces; 32 bytes, base64url
4848- # tip: run the `gen-keys` binary to generate this
4949- # - PDS_DPOP_NONCE_SECRET=
1010+ - PDS_LOG_LEVEL=${PDS_LOG_LEVEL:-info}
1111+ - PDS_DATA_DIR=${PDS_DATA_DIR:-./data}
1212+ - PDS_HOSTNAME=${PDS_HOSTNAME:?}
1313+ - PDS_DID=${PDS_DID}
1414+ - PDS_INVITE_CODE_REQUIRED=${PDS_INVITE_CODE_REQUIRED:-true}
1515+ - PDS_ROTATION_KEY=${PDS_ROTATION_KEY:?}
1616+ - PDS_JWK_MULTIBASE=${PDS_JWK_MULTIBASE:?}
1717+ - PDS_ADMIN_PASSWORD=${PDS_ADMIN_PASSWORD:?}
1818+ - PDS_CRAWLERS=${PDS_CRAWLERS:-https://bsky.network}
1919+ - PDS_DPOP_NONCE_SECRET=${PDS_DPOP_NONCE_SECRET}
50205151- # (optional) email config (see readme for details)
5252- # auth uri should look like smtp[s]://user:pass@host[:port]
5353- # sender should look like e@mail.com or Name <e@mail.com>
5454- # without setting these, emails will be logged to stdout
5555- # - PDS_SMTP_STARTTLS=false
5656- # - PDS_SMTP_AUTH_URI=
5757- # - PDS_SMTP_SENDER=
2121+ - PDS_SMTP_STARTTLS=${PDS_SMTP_STARTTLS:-false}
2222+ - PDS_SMTP_AUTH_URI=${PDS_SMTP_AUTH_URI}
2323+ - PDS_SMTP_SENDER=${PDS_SMTP_SENDER}
58245959- # (optional) S3 config (see readme for details)
6060- # set these to back up your databases and/or blobs to S3(-compatible storge)
6161- # - PDS_S3_BLOBS_ENABLED=false
6262- # - PDS_S3_BACKUPS_ENABLED=false
6363- # - PDS_S3_BACKUP_INTERVAL_S=3600
6464- # - PDS_S3_ENDPOINT=
6565- # - PDS_S3_REGION=
6666- # - PDS_S3_BUCKET=
6767- # - PDS_S3_ACCESS_KEY=
6868- # - PDS_S3_SECRET_KEY=
6969- # - PDS_S3_CDN_URL=
2525+ - PDS_S3_BLOBS_ENABLED=${PDS_S3_BLOBS_ENABLED:-false}
2626+ - PDS_S3_BACKUPS_ENABLED=${PDS_S3_BACKUPS_ENABLED:-false}
2727+ - PDS_S3_BACKUP_INTERVAL_S=${PDS_S3_BACKUP_INTERVAL_S:-3600}
2828+ - PDS_S3_ENDPOINT=${PDS_S3_ENDPOINT}
2929+ - PDS_S3_REGION=${PDS_S3_REGION}
3030+ - PDS_S3_BUCKET=${PDS_S3_BUCKET}
3131+ - PDS_S3_ACCESS_KEY=${PDS_S3_ACCESS_KEY}
3232+ - PDS_S3_SECRET_KEY=${PDS_S3_SECRET_KEY}
3333+ - PDS_S3_CDN_URL=${PDS_S3_CDN_URL}
7034 restart: unless-stopped
71357236 caddy:
···7943 - caddy-data:/data
8044 - caddy-config:/config
8145 environment:
8282- - PDS_HOSTNAME=
4646+ - PDS_HOSTNAME=${PDS_HOSTNAME:?}
8347 restart: unless-stopped
84488549volumes:
+73-68
pegasus/lib/env.ml
···22 try Sys.getenv name
33 with Not_found -> failwith ("Missing environment variable " ^ name)
4455+let getenv_opt name ~default =
66+ match Sys.getenv_opt name with
77+ | Some value when value <> "" ->
88+ value
99+ | _ ->
1010+ default
1111+512let log_level =
66- match Sys.getenv_opt "PDS_LOG_LEVEL" |> Option.map String.lowercase_ascii with
77- | Some "debug" ->
1313+ match getenv_opt "PDS_LOG_LEVEL" ~default:"info" with
1414+ | "debug" ->
815 `Debug
99- | Some "info" ->
1010- `Info
1111- | Some "warn" | Some "warning" ->
1616+ | "warn" | "warning" ->
1217 `Warning
1313- | Some "error" ->
1818+ | "error" ->
1419 `Error
1520 | _ ->
1621 `Info
17221818-let data_dir = Option.value ~default:"./data" @@ Sys.getenv_opt "PDS_DATA_DIR"
2323+let data_dir = getenv_opt "PDS_DATA_DIR" ~default:"./data"
19242025let hostname = getenv "PDS_HOSTNAME"
21262227let host_endpoint = "https://" ^ hostname
23282424-let did =
2525- Option.value ~default:("did:web:" ^ hostname) @@ Sys.getenv_opt "PDS_DID"
2929+let did = getenv_opt "PDS_DID" ~default:("did:web:" ^ hostname)
26302731let invite_required = getenv "PDS_INVITE_CODE_REQUIRED" = "true"
2832···3640let admin_password = getenv "PDS_ADMIN_PASSWORD"
37413842let crawlers =
3939- Sys.getenv_opt "PDS_CRAWLERS"
4040- |> Option.value ~default:"https://bsky.network"
4343+ getenv_opt "PDS_CRAWLERS" ~default:"https://bsky.network"
4144 |> String.split_on_char ','
4245 |> List.map (fun u ->
4346 match Uri.of_string @@ String.trim u with
···4750 Uri.make ~scheme:"https" ~host:u () )
48514952let dpop_nonce_secret =
5050- match Sys.getenv_opt "PDS_DPOP_NONCE_SECRET" with
5151- | Some sec ->
5353+ match getenv_opt "PDS_DPOP_NONCE_SECRET" ~default:"" with
5454+ | "" ->
5555+ let secret = Mirage_crypto_rng_unix.getrandom 32 in
5656+ Dream.warning (fun log ->
5757+ log "PDS_DPOP_NONCE_SECRET not set; using PDS_DPOP_NONCE_SECRET=%s"
5858+ ( Base64.(encode ~alphabet:uri_safe_alphabet ~pad:false) secret
5959+ |> Result.get_ok ) ) ;
6060+ Bytes.of_string secret
6161+ | sec ->
5262 let secret =
5363 Base64.(decode_exn ~alphabet:uri_safe_alphabet ~pad:false) sec
5464 |> Bytes.of_string
5565 in
5666 if Bytes.length secret = 32 then secret
5767 else failwith "PDS_DPOP_NONCE_SECRET must be 32 bytes in base64uri"
5858- | None ->
5959- let secret = Mirage_crypto_rng_unix.getrandom 32 in
6060- Dream.warning (fun log ->
6161- log "PDS_DPOP_NONCE_SECRET not set; using PDS_DPOP_NONCE_SECRET=%s"
6262- ( Base64.(encode ~alphabet:uri_safe_alphabet ~pad:false) secret
6363- |> Result.get_ok ) ) ;
6464- Bytes.of_string secret
65686669let smtp_config, smtp_sender =
6770 begin
6871 let with_starttls =
6969- Option.value ~default:"false" @@ Sys.getenv_opt "PDS_SMTP_STARTTLS"
7070- = "true"
7272+ getenv_opt "PDS_SMTP_STARTTLS" ~default:"false" = "true"
7173 in
7274 match
7373- ( Option.map Uri.of_string (Sys.getenv_opt "PDS_SMTP_AUTH_URI")
7474- , Sys.getenv_opt "PDS_SMTP_SENDER" )
7575+ ( getenv_opt "PDS_SMTP_AUTH_URI" ~default:""
7676+ , getenv_opt "PDS_SMTP_SENDER" ~default:"" )
7577 with
7676- | Some uri, Some sender -> (
7777- match
7878- ( Uri.scheme uri
7979- , Uri.user uri
8080- , Uri.password uri
8181- , Uri.host uri
8282- , Uri.port uri )
8383- with
8484- | Some scheme, Some username, Some password, Some hostname, port
8585- when scheme = "smtp" || scheme = "smtps" -> (
8686- match Emile.of_string sender with
8787- | Ok _ ->
8888- ( Some
8989- Letters.Config.(
9090- create ~username ~password ~hostname ~with_starttls ()
9191- |> set_port port )
9292- , Some sender )
9393- | Error _ ->
9494- failwith
9595- "PDS_SMTP_SENDER should be a valid mailbox, e.g. `e@mail.com` or \
9696- `Name <e@mail.com>`" )
9797- | _ ->
9898- failwith
9999- "PDS_SMTP_AUTH_URI must be a valid smtp:// or smtps:// URI with \
100100- username, password, and hostname" )
101101- | Some _, None ->
7878+ | "", "" ->
7979+ (None, None)
8080+ | _uri, "" ->
10281 failwith
10382 "PDS_SMTP_SENDER must be set alongside PDS_SMTP_AUTH_URI; it should \
10483 look like `e@mail.com` or `Name <e@mail.com>`"
105105- | None, Some _ ->
8484+ | "", _uri ->
10685 failwith "PDS_SMTP_AUTH_URI must be set alongside PDS_SMTP_SENDER"
107107- | None, None ->
108108- (None, None)
8686+ | uri, sender -> (
8787+ let uri = Uri.of_string uri in
8888+ match
8989+ ( Uri.scheme uri
9090+ , Uri.user uri
9191+ , Uri.password uri
9292+ , Uri.host uri
9393+ , Uri.port uri )
9494+ with
9595+ | Some scheme, Some username, Some password, Some hostname, port
9696+ when scheme = "smtp" || scheme = "smtps" -> (
9797+ match Emile.of_string sender with
9898+ | Ok _ ->
9999+ ( Some
100100+ Letters.Config.(
101101+ create ~username ~password ~hostname ~with_starttls ()
102102+ |> set_port port )
103103+ , Some sender )
104104+ | Error _ ->
105105+ failwith
106106+ "PDS_SMTP_SENDER should be a valid mailbox, e.g. `e@mail.com` \
107107+ or `Name <e@mail.com>`" )
108108+ | _ ->
109109+ failwith
110110+ "PDS_SMTP_AUTH_URI must be a valid smtp:// or smtps:// URI with \
111111+ username, password, and hostname" )
109112 end
110113111114type s3_config =
···125128 begin
126129 let default_backup_interval = 3600.0 in
127130 let blobs_enabled =
128128- Sys.getenv_opt "PDS_S3_BLOBS_ENABLED" |> Option.map (( = ) "true")
131131+ getenv_opt "PDS_S3_BLOBS_ENABLED" ~default:"false" = "true"
129132 in
130133 let backups_enabled =
131131- Sys.getenv_opt "PDS_S3_BACKUPS_ENABLED" |> Option.map (( = ) "true")
134134+ getenv_opt "PDS_S3_BACKUPS_ENABLED" ~default:"false" = "true"
132135 in
133136 let backup_interval_s =
134134- Sys.getenv_opt "PDS_S3_BACKUP_INTERVAL_S"
135135- |> Option.map float_of_string_opt
136136- |> Option.join
137137+ getenv_opt "PDS_S3_BACKUP_INTERVAL_S"
138138+ ~default:(string_of_float default_backup_interval)
139139+ |> float_of_string
137140 in
138141 let endpoint = Sys.getenv_opt "PDS_S3_ENDPOINT" in
139142 match (blobs_enabled, backups_enabled) with
140140- | Some true, _ | _, Some true -> (
143143+ | true, _ | _, true -> (
141144 match
142145 ( Sys.getenv_opt "PDS_S3_REGION"
143146 , Sys.getenv_opt "PDS_S3_BUCKET"
144147 , Sys.getenv_opt "PDS_S3_ACCESS_KEY"
145145- , Sys.getenv_opt "PDS_S3_SECRET_KEY" )
148148+ , Sys.getenv_opt "PDS_S3_SECRET_KEY"
149149+ , Sys.getenv_opt "PDS_S3_CDN_URL" )
146150 with
147147- | Some region, Some bucket, Some access_key, Some secret_key ->
151151+ | Some region, Some bucket, Some access_key, Some secret_key, cdn_url ->
148152 let region_obj =
149153 match endpoint with
150154 | Some host ->
···155159 let endpoint_obj =
156160 Aws_s3.Region.endpoint ~inet:`V4 ~scheme:`Https region_obj
157161 in
162162+ let credentials_obj =
163163+ Aws_s3.Credentials.make ~access_key ~secret_key ()
164164+ in
158165 Some
159159- { blobs_enabled= Option.value ~default:false blobs_enabled
160160- ; backups_enabled= Option.value ~default:false backups_enabled
161161- ; backup_interval_s=
162162- Option.value ~default:default_backup_interval backup_interval_s
166166+ { blobs_enabled
167167+ ; backups_enabled
168168+ ; backup_interval_s
163169 ; endpoint
164170 ; region
165171 ; bucket
166172 ; access_key
167173 ; secret_key
168168- ; cdn_url= Sys.getenv_opt "PDS_S3_CDN_URL"
169169- ; credentials_obj=
170170- Aws_s3.Credentials.make ~access_key ~secret_key ()
174174+ ; cdn_url
175175+ ; credentials_obj
171176 ; endpoint_obj }
172177 | _ ->
173178 failwith