Server: GET /voip/turnServer, configured under client_api.turn. A shared
secret (coturn use-auth-secret) gives each user expiring TURN REST API
credentials; static credentials are the fallback; with nothing configured
it answers {} so clients still try a direct connection.
Web client: 1:1 calls (Matrix VoIP via matrix-js-sdk) in rooms with one
other person. Voice and video buttons in the room header, a ringing card
with answer, answer without video and decline, and a call panel that
stays up across rooms: mute, camera, screen sharing, expand, hang up,
ring tones. Call events show in the timeline and room list. Works around
the SDK keeping a failed microphone request forever.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
230 lines
17 KiB
Markdown
230 lines
17 KiB
Markdown
# Configuration
|
|
|
|
Warp reads one YAML file. Every setting can also come from the
|
|
environment, and the environment wins. With no file at all the defaults
|
|
plus `WARP_*` variables are used, which is how the container image runs.
|
|
|
|
Where the file comes from, in order:
|
|
|
|
1. `--config <path>`
|
|
2. `$WARP_CONFIG`
|
|
3. `./warp.yaml`, if it exists
|
|
4. nothing — defaults and environment only
|
|
|
|
An unknown field is a fatal error. A typo in a security-relevant setting
|
|
must not be ignored.
|
|
|
|
List-valued variables are comma-separated. A single `-` clears a list:
|
|
`WARP_CORS_ALLOWED_ORIGINS=-`.
|
|
|
|
## Settings
|
|
|
|
| YAML | Environment | Default | Notes |
|
|
|---|---|---|---|
|
|
| `global.server_name` | `WARP_SERVER_NAME` | — | Required. Part of every ID; cannot be changed later. |
|
|
| `global.private_key` | `WARP_PRIVATE_KEY` | `./keys/signing.key` | Written by `warp generate-keys`. |
|
|
| `global.well_known.client_base_url` | `WARP_WELL_KNOWN_CLIENT_BASE_URL` | `https://<server_name>` | Advertised to clients. |
|
|
| `global.well_known.identity_server_base_url` | `WARP_WELL_KNOWN_IDENTITY_SERVER_BASE_URL` | empty | Omitted from the document when empty. |
|
|
| `global.well_known.server_address` | `WARP_WELL_KNOWN_SERVER_ADDRESS` | empty | `host:port` for other homeservers. Empty = no delegation, other servers fall back to SRV/A. |
|
|
| `listen.address` | `WARP_LISTEN_ADDRESS` | `:8008` | |
|
|
| `listen.read_header_timeout` | `WARP_READ_HEADER_TIMEOUT` | `10s` | |
|
|
| `listen.read_timeout` | `WARP_READ_TIMEOUT` | `60s` | |
|
|
| `listen.write_timeout` | `WARP_WRITE_TIMEOUT` | `120s` | Must stay above `sync.long_poll_timeout`, or a blocked `/sync` is cut off before it can answer. |
|
|
| `listen.idle_timeout` | `WARP_IDLE_TIMEOUT` | `120s` | |
|
|
| `listen.max_header_bytes` | `WARP_MAX_HEADER_BYTES` | `1048576` | |
|
|
| `listen.shutdown_timeout` | `WARP_SHUTDOWN_TIMEOUT` | `30s` | Grace period for in-flight requests. |
|
|
| `listen.federation_address` | `WARP_LISTEN_FEDERATION_ADDRESS` | empty | A second listener serving the same API over TLS, usually `:8448`. Empty = none; see *Federation over TLS*. |
|
|
| `listen.tls_cert_file` | `WARP_TLS_CERT_FILE` | empty | PEM certificate chain for the federation listener. Required with it. |
|
|
| `listen.tls_key_file` | `WARP_TLS_KEY_FILE` | empty | PEM key for the federation listener. Required with it. |
|
|
| `database.driver` | `WARP_DB_DRIVER` | `sqlite` | `sqlite`, `postgres`, `pocketbase`. |
|
|
| `database.connection_string` | `WARP_DB_CONNECTION_STRING` | SQLite file in the working directory | A DSN for postgres, a `file:` URI for sqlite, the data directory (PocketBase's `pb_data`) for pocketbase. |
|
|
| `database.max_open_conns` | `WARP_DB_MAX_OPEN_CONNS` | `10` | Ignored by SQLite: writes are serialised onto one connection. |
|
|
| `database.max_idle_conns` | `WARP_DB_MAX_IDLE_CONNS` | `5` | |
|
|
| `database.conn_max_lifetime` | `WARP_DB_CONN_MAX_LIFETIME` | `1h` | |
|
|
| `database.auto_migrate` | `WARP_DB_AUTO_MIGRATE` | `true` | `false` makes startup fail while migrations are pending. |
|
|
| `database.pocketbase.s3.enabled` | `WARP_PB_S3_ENABLED` | `false` | pocketbase only: media content goes to an S3-compatible bucket instead of `pb_data/storage`. |
|
|
| `database.pocketbase.s3.bucket` / `.region` / `.endpoint` | `WARP_PB_S3_BUCKET` / `WARP_PB_S3_REGION` / `WARP_PB_S3_ENDPOINT` | empty | All required when S3 is on. |
|
|
| `database.pocketbase.s3.access_key` / `.secret` | `WARP_PB_S3_ACCESS_KEY` / `WARP_PB_S3_SECRET` | empty | All required when S3 is on. |
|
|
| `database.pocketbase.s3.force_path_style` | `WARP_PB_S3_FORCE_PATH_STYLE` | `false` | For MinIO and other servers without virtual-host buckets. |
|
|
| `database.pocketbase.s3.enabled` | `WARP_PB_S3_ENABLED` | `false` | pocketbase only: media content in an S3-compatible bucket instead of `pb_data/storage`. |
|
|
| `database.pocketbase.s3.bucket` / `.region` / `.endpoint` | `WARP_PB_S3_BUCKET` / `WARP_PB_S3_REGION` / `WARP_PB_S3_ENDPOINT` | empty | All required with S3 on. |
|
|
| `database.pocketbase.s3.access_key` / `.secret` | `WARP_PB_S3_ACCESS_KEY` / `WARP_PB_S3_SECRET` | empty | All required with S3 on. |
|
|
| `database.pocketbase.s3.force_path_style` | `WARP_PB_S3_FORCE_PATH_STYLE` | `false` | For MinIO and other servers without virtual-host buckets. |
|
|
| `client_api.cors_allowed_origins` | `WARP_CORS_ALLOWED_ORIGINS` | `["*"]` | The spec expects any origin. |
|
|
| `client_api.trust_x_forwarded_for` | `WARP_TRUST_X_FORWARDED_FOR` | `false` | Only behind a proxy that overwrites the header; the rate limiter keys on the result. |
|
|
| `client_api.access_token_lifetime` | `WARP_ACCESS_TOKEN_LIFETIME` | `0s` | `0` = never expire. With a lifetime, clients may ask for a refresh token at login. |
|
|
| `client_api.refresh_token_lifetime` | `WARP_REFRESH_TOKEN_LIFETIME` | `0s` | `0` = never expire. |
|
|
| `client_api.rate_limit.enabled` | `WARP_RATE_LIMIT_ENABLED` | `true` | |
|
|
| `client_api.rate_limit.default.burst` / `.interval` | — | `30` / `100ms` | Every rate-limited endpoint without its own bucket. |
|
|
| `client_api.rate_limit.login.burst` / `.interval` | — | `10` / `1s` | `/login` and `/refresh`. |
|
|
| `client_api.rate_limit.register.burst` / `.interval` | — | `5` / `2s` | `/register` and `/register/available`. |
|
|
| `client_api.rate_limit.room_create.burst` / `.interval` | — | `10` / `1s` | `/createRoom`, which writes a dozen events per call. |
|
|
| `client_api.rate_limit.join.burst` / `.interval` | — | `10` / `1s` | The two join endpoints. |
|
|
| `client_api.rate_limit.sync.burst` / `.interval` | — | `100` / `20ms` | `/sync` and the writes behind it: typing, receipts, to-device, presence. |
|
|
| `client_api.rate_limit.media.burst` / `.interval` | — | `100` / `20ms` | Media downloads and thumbnails: a client opening a room asks for every avatar at once. Uploads are in the default bucket. |
|
|
| `client_api.turn.uris` | `WARP_TURN_URIS` | `[]` | `turn:`, `turns:` and `stun:` URIs for calls. Empty = no relay; `/voip/turnServer` answers `{}`. |
|
|
| `client_api.turn.shared_secret` | `WARP_TURN_SHARED_SECRET` | — | coturn's `static-auth-secret`: each user gets expiring TURN REST API credentials. |
|
|
| `client_api.turn.user_lifetime` | `WARP_TURN_USER_LIFETIME` | `1h` | How long shared-secret credentials last. |
|
|
| `client_api.turn.username` / `.password` | `WARP_TURN_USERNAME` / `WARP_TURN_PASSWORD` | — | Static credentials, used only without a shared secret. |
|
|
| `sync.presence` | `WARP_SYNC_PRESENCE` | `false` | Off by default; see below. |
|
|
| `sync.typing_timeout` | `WARP_SYNC_TYPING_TIMEOUT` | `30s` | How long a typing notification lasts, and the cap on what a client may ask for. |
|
|
| `sync.long_poll_timeout` | `WARP_SYNC_LONG_POLL_TIMEOUT` | `30s` | The cap on a `/sync` `?timeout=`. |
|
|
| `federation.enabled` | `WARP_FEDERATION_ENABLED` | `false` | |
|
|
| `federation.allowlist` | `WARP_FEDERATION_ALLOWLIST` | `[]` | Non-empty = federate only with these. |
|
|
| `federation.denylist` | `WARP_FEDERATION_DENYLIST` | `[]` | Always wins over the allowlist. |
|
|
| `federation.allow_runtime_toggle` | `WARP_FEDERATION_ALLOW_RUNTIME_TOGGLE` | `true` | See below. |
|
|
| `federation.trusted_key_servers` | `WARP_FEDERATION_TRUSTED_KEY_SERVERS` | `[]` | Notaries asked for a server's key when it cannot be reached itself, e.g. `matrix.org`. Empty = only ever ask the server. |
|
|
| `federation.key_validity_period` | `WARP_FEDERATION_KEY_VALIDITY_PERIOD` | `24h` | How long other servers may cache this server's key. At most `168h`. |
|
|
| `federation.tls_ca_file` | `WARP_FEDERATION_TLS_CA_FILE` | empty | Extra CA certificates (PEM) for outbound federation, on top of the system roots. |
|
|
| `media.base_path` | `WARP_MEDIA_BASE_PATH` | `./media_store` | Where media content is kept. Created on the first upload. |
|
|
| `media.max_upload_bytes` | `WARP_MEDIA_MAX_UPLOAD_BYTES` | `52428800` | The largest upload (50 MiB), and the largest file fetched from another server. Reported to clients as `m.upload.size`. |
|
|
| `media.max_user_storage_bytes` | `WARP_MEDIA_MAX_USER_STORAGE_BYTES` | `0` | The most one user may have uploaded in all. `0` = no limit. The admin API can give a user a quota of their own. |
|
|
| `media.max_thumbnail_pixels` | `WARP_MEDIA_MAX_THUMBNAIL_PIXELS` | `32000000` | The largest image, in pixels, decoded to make a thumbnail. |
|
|
| `media.thumbnail_sizes` | — | five sizes, see [media.md](media.md) | The thumbnails this server makes, as `{width, height, method}`. Empty = no thumbnails. |
|
|
| `media.pending_upload_expiry` | `WARP_MEDIA_PENDING_UPLOAD_EXPIRY` | `24h` | How long an ID reserved with `POST /media/v1/create` waits for its content. |
|
|
| `media.max_pending_uploads` | `WARP_MEDIA_MAX_PENDING_UPLOADS` | `10` | How many of those one user may hold. |
|
|
| `media.legacy_unauthenticated_downloads` | `WARP_MEDIA_LEGACY_UNAUTHENTICATED_DOWNLOADS` | `false` | Also serve the deprecated `/_matrix/media/v3/download` and `/thumbnail`, which need no token. Only for clients older than Matrix 1.11. |
|
|
| `push.enabled` | `WARP_PUSH_ENABLED` | `true` | Send pushes to the push gateways of users' pushers. Off, notifications are still counted. |
|
|
| `push.include_content` | `WARP_PUSH_INCLUDE_CONTENT` | `true` | Put the event's content into a push. Off, a push carries no message text. |
|
|
| `push.gateway_timeout` | `WARP_PUSH_GATEWAY_TIMEOUT` | `10s` | One request to a push gateway. |
|
|
| `push.notification_retention` | `WARP_PUSH_NOTIFICATION_RETENTION` | `720h` | How long a read notification is kept for `GET /notifications`. Unread ones are kept until read. |
|
|
| `registration.enabled` | `WARP_REGISTRATION_ENABLED` | `false` | |
|
|
| `registration.allow_runtime_toggle` | `WARP_REGISTRATION_ALLOW_RUNTIME_TOGGLE` | `true` | |
|
|
| `registration.tokens` | `WARP_REGISTRATION_TOKENS` | `[]` | Non-empty = sign-up also needs a token, as the `m.login.registration_token` stage. These never run out; tokens with a use limit or an expiry are made through the admin API, and both kinds are accepted. |
|
|
| `registration.require_token` | `WARP_REGISTRATION_REQUIRE_TOKEN` | `false` | Ask for a token even with no `tokens` above, for a server whose tokens all come from the admin API. |
|
|
| `appservice.config_files` | `WARP_APPSERVICE_CONFIG_FILES` | `[]` | Application service registration files, read at startup. A bad one stops the server. See [appservices.md](appservices.md). |
|
|
| `appservice.timeout` | `WARP_APPSERVICE_TIMEOUT` | `30s` | One request to an application service: a transaction, a query or a ping. |
|
|
| `server_notices.enabled` | `WARP_SERVER_NOTICES_ENABLED` | `false` | Create the notices user at startup and serve `POST /_warp/admin/v1/server_notices`. Needs a signing key. |
|
|
| `server_notices.localpart` | `WARP_SERVER_NOTICES_LOCALPART` | `server-notices` | The user notices come from. Created if it does not exist, so pick a name no real user has. |
|
|
| `server_notices.display_name` | `WARP_SERVER_NOTICES_DISPLAY_NAME` | `Server Notices` | Its display name, kept in step with this setting at startup. |
|
|
| `server_notices.room_name` | `WARP_SERVER_NOTICES_ROOM_NAME` | `Server Notices` | The name of the room each user gets their notices in. |
|
|
| `logging.level` | `WARP_LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error`. |
|
|
| `logging.format` | `WARP_LOG_FORMAT` | `text` | `text` or `json`. |
|
|
| `logging.add_source` | `WARP_LOG_ADD_SOURCE` | `false` | File and line in every record. |
|
|
| `metrics.enabled` | `WARP_METRICS_ENABLED` | `true` | |
|
|
| `metrics.path` | `WARP_METRICS_PATH` | `/metrics` | Served on the same listener. |
|
|
| `tracing.enabled` | `WARP_TRACING_ENABLED` | `false` | OpenTelemetry tracing; see [deployment.md](deployment.md#tracing). |
|
|
| `tracing.endpoint` | `WARP_TRACING_ENDPOINT` | empty | The OTLP/HTTP collector, e.g. `http://localhost:4318`. Empty falls back to `OTEL_EXPORTER_OTLP_ENDPOINT`, then to `localhost:4318`. |
|
|
| `tracing.sample_ratio` | `WARP_TRACING_SAMPLE_RATIO` | `1` | The fraction of new traces kept, 0 to 1. A request that arrives inside a sampled trace is always kept. |
|
|
| `tracing.service_name` | `WARP_TRACING_SERVICE_NAME` | `warp` | |
|
|
|
|
The rate limit buckets share one Go type, so per-bucket environment
|
|
variables would all collide on the same name; they are YAML only.
|
|
`WARP_RATE_LIMIT_ENABLED=false` turns the whole limiter off, which is what a
|
|
test harness wants.
|
|
|
|
## Rate limiting
|
|
|
|
Each bucket allows `burst` requests back to back and returns one slot every
|
|
`interval`, keyed per client address. Over the limit is
|
|
`429 M_LIMIT_EXCEEDED` with a `retry_after_ms` the client is expected to
|
|
honour.
|
|
|
|
Buckets are per endpoint class, so a client hammering `/login` cannot spend
|
|
the budget of `/profile`. The key is the client address and not the user,
|
|
because the limiter runs before the access token is resolved — a limiter
|
|
that had to query the database to find its own key would be a denial of
|
|
service of its own. Behind a reverse proxy, set
|
|
`client_api.trust_x_forwarded_for: true`, or every client shares one bucket.
|
|
|
|
The admin API under `/_warp/admin/v1` is not rate limited: it needs an admin
|
|
token, and bulk operations from the panel are exactly the traffic a limiter
|
|
would get in the way of.
|
|
|
|
## Presence
|
|
|
|
`sync.presence` is off by default. Presence — who is online, and the status
|
|
message they set — is the noisiest thing in Matrix: every user's state
|
|
change is published to everybody they share a room with, which over
|
|
federation is quadratic in the worst case. Synapse deployments routinely
|
|
turn it off for that reason, and a private homeserver with a dozen users
|
|
gains little from it.
|
|
|
|
With it off:
|
|
|
|
- `GET` and `PUT /presence/{userId}/status` answer `403 M_FORBIDDEN`. They
|
|
are deliberately not unregistered: a client that gets a 404 from
|
|
`/presence` concludes the server is too old to have it and stops asking,
|
|
where a refusal says this one has it switched off.
|
|
- The `presence` section of a `/sync` response is empty.
|
|
- `?set_presence=` on `/sync` is ignored rather than refused — it rides on
|
|
every sync a client sends, and failing them all would make the server
|
|
unusable.
|
|
|
|
It is a boot-time setting, not a runtime one: turning it on starts
|
|
publishing a stream that nothing was tracking before, and a server that
|
|
could do that mid-flight would owe every connected client a resync.
|
|
|
|
## Long polling
|
|
|
|
`/sync` holds a request open until something happens or the timeout
|
|
expires, so `listen.write_timeout` has to stay above
|
|
`sync.long_poll_timeout` — otherwise the listener cuts off a blocked sync
|
|
before it can answer, and the client sees a dropped connection instead of
|
|
an empty response.
|
|
|
|
Raising `sync.long_poll_timeout` does not make a client hear about messages
|
|
sooner: a blocked sync is woken the moment anything concerning it is
|
|
written. All the timeout decides is how often an idle client
|
|
re-establishes its request.
|
|
|
|
## Settings that change at runtime
|
|
|
|
`federation.enabled` and `registration.enabled` are also stored in the
|
|
`server_settings` database table, which is what the admin API writes to.
|
|
|
|
- With `allow_runtime_toggle: true` (the default) the stored value wins
|
|
over the config file at boot, and the admin API may change it. The
|
|
change takes effect on the next request; no restart.
|
|
- With `allow_runtime_toggle: false` the config file is authoritative and
|
|
the admin API refuses to change the setting. Any previously stored value
|
|
is kept but ignored, so re-enabling the toggle restores the
|
|
administrator's last choice.
|
|
|
|
Everything else is read once at startup.
|
|
|
|
## What federation off actually means
|
|
|
|
While `federation.enabled` is false:
|
|
|
|
- `/_matrix/federation/*`, `/_matrix/key/*` and `/.well-known/matrix/server`
|
|
answer `404 M_UNRECOGNIZED`, exactly as on a server with no federation
|
|
API. The TLS listener on `listen.federation_address`, if there is one,
|
|
stays up and answers the same way.
|
|
- Nothing outbound runs. The transaction queue is stopped, and the
|
|
federation client refuses every request — key fetches, joins, profile
|
|
and alias lookups — before it opens a connection, so a component that
|
|
somehow missed the switch still cannot reach another server.
|
|
- Events persisted while it is off are not queued for other servers. A
|
|
remote server that cares about the room fetches what it missed the next
|
|
time it hears from this one. What was already queued when the switch
|
|
went off stays queued, and goes out when it comes back on.
|
|
- Client-Server endpoints that would need a remote server answer
|
|
`M_FORBIDDEN` with a message saying federation is disabled. That
|
|
includes downloading media from another server that is not already
|
|
cached here; media fetched before the switch went off is still served.
|
|
- Events are still signed with the server key, and room and event IDs are
|
|
unchanged. A room created while federation is off federates normally
|
|
once it is switched on.
|
|
|
|
## Federation over TLS
|
|
|
|
Other servers reach this one at whatever its server name resolves to: the
|
|
address in `/.well-known/matrix/server`, an SRV record, or the name itself
|
|
on port 8448. Two deployment shapes:
|
|
|
|
- **Behind a reverse proxy** that terminates TLS — the usual one. Set
|
|
`global.well_known.server_address` to the proxy's `host:443` and leave
|
|
`listen.federation_address` empty: the proxy forwards
|
|
`/_matrix/federation` and `/_matrix/key` to `listen.address` like
|
|
everything else.
|
|
- **Warp terminates TLS itself**: set `listen.federation_address` to
|
|
`:8448` with `listen.tls_cert_file` and `listen.tls_key_file`. The
|
|
certificate must be valid for the server name (or for the delegated
|
|
name, if the well-known document delegates).
|
|
|
|
Outbound federation verifies certificates against the system roots, plus
|
|
`federation.tls_ca_file` when it is set — for a private federation whose
|
|
servers share an internal CA. There is no switch to turn verification
|
|
off.
|