diff --git a/Configuration.md b/Configuration.md
new file mode 100644
index 0000000..266b227
--- /dev/null
+++ b/Configuration.md
@@ -0,0 +1,103 @@
+# Configuration
+
+Echo reads a single TOML file. Pass its path as the first argument (default `config.toml`). Copy `config.example.toml` to start. `config.toml` is gitignored because it holds the link password.
+
+## [uplink] (required)
+
+The ircd Echo links to. `password` must match the `` block on the uplink.
+
+ [uplink]
+ host = "127.0.0.1"
+ port = 7000
+ password = "changeme"
+
+## [server] (required)
+
+Echo's own identity on the network. `sid` is three characters, unique on the network. `protocol` is the InspIRCd link version (1206 for insp4, 1205 for insp3).
+
+ [server]
+ name = "services.example.net"
+ sid = "42S"
+ description = "Federated Services"
+ protocol = 1206
+
+## [gossip], [gossip.tls], [[peer]]
+
+Node-to-node replication. Omit all three to run a single node. See [Federation](Federation).
+
+ [gossip]
+ bind = "0.0.0.0:16700"
+ secret = "shared-secret"
+
+ [gossip.tls] # omit for a plaintext link
+ cert = "certs/node.crt"
+ key = "certs/node.key"
+ ca = "certs/ca.crt" # a peer must present a cert signed by this CA
+
+ [[peer]] # one block per other node
+ addr = "other-node:16700"
+ name = "other-node" # TLS name to expect; matches the peer certificate
+
+Generate certificates with `scripts/gen-certs.sh`.
+
+## [grpc], [grpc.tls]
+
+The account and channel directory over gRPC, for a website to mirror. Omit to disable. See [Directory API](Directory-API).
+
+ [grpc]
+ bind = "127.0.0.1:50051"
+ token = "shared-secret" # every RPC sends `authorization: Bearer `
+
+## [modules]
+
+Which services start. Omit for the full standard suite. List names to run a subset; add `"example"` for the template service.
+
+ [modules]
+ services = ["nickserv", "chanserv"]
+
+## [[oper]]
+
+Services operators and their privileges: `auspex` (see hidden info), `suspend` (suspend/unsuspend), `admin` (modify or drop others' accounts and channels).
+
+ [[oper]]
+ account = "yournick"
+ privs = ["auspex", "suspend", "admin"]
+
+## [log]
+
+An audit channel for the incident feed. Notable actions are announced there; private material and cosmetic self-service tweaks are not.
+
+ [log]
+ channel = "#services"
+
+## [expire]
+
+Inactivity expiry. Accounts not identified to, and channels not joined, past the threshold are dropped on a periodic pass. Opers, live sessions, occupied channels, and NOEXPIRE records are spared. A zero or omitted field leaves that kind never expiring.
+
+ [expire]
+ accounts_days = 90
+ channels_days = 30
+ warn_days = 7 # heads-up email this many days before expiry
+
+## [session]
+
+Per-IP session limiting. The connection that puts an IP over the limit is killed on connect; OperServ exceptions adjust it per mask.
+
+ [session]
+ default_limit = 3
+
+## [auth]
+
+Account authority. Omitted, Echo owns accounts itself. With `external = true`, identity is handed to an outside authority (for example a website): IRC can only IDENTIFY, and the authority pushes accounts in via the gRPC Accounts API. Echo still owns channel, vhost, and ban data.
+
+ [auth]
+ external = true
+
+## [email]
+
+Outbound email for registration confirmation and password recovery. Omit to run without email. `command` is a shell command fed the message on stdin (`msmtp -t`, `sendmail -t`, or a relay script).
+
+ [email]
+ command = "msmtp -t"
+ from = "noreply@example.net"
+ brand = "Example Network"