-
Notifications
You must be signed in to change notification settings - Fork 0
Configuration
Everything is environment variables in .env at the repository root.
.env.exampleis the authoritative reference. Every variable is documented where it is defined, next to its default, and it ships with the code so it can never drift from what the app reads. This page is the map: what matters, in what order, and which settings interact.
-
docker/docker-compose.ymlpins a few values that must not be overridable — the storage paths inside the container (WRIT_DB_PATH,WRIT_FILES_DIR), because they have to match the volume mount. - Everything else comes from
.env. - Two settings are also editable at runtime in the UI, under Settings → Network, and take effect without a restart: the public URL and the trusted-host list. Once saved there, the stored value wins over the env on subsequent boots.
./scripts/gen-env.sh generates all of these. You should never write them by
hand.
| Variable | What it protects |
|---|---|
WRIT_JWT_SECRET |
signs session tokens (mapped onto JWT_SECRET_KEY) |
API_SECRET_KEY |
fallback token-signing key |
HMAC_SECRET_KEY |
webhook / request signatures |
RECORDER_AUTH_SECRET |
agent tokens |
INTERNAL_API_SECRET |
internal service endpoints |
GATEWAY_SECRET |
gateway service-to-service calls |
DOC_EXTRACT_SECRET |
the document/OCR service |
SECRET_ENCRYPTION_KEY |
encrypts your stored credentials at rest |
With ENVIRONMENT=production (the default) the coordinator refuses to boot
if any is missing, blank, shorter than 32 characters, or still a shipped default.
That is deliberate: HS256 signs happily with an empty key, so a half-filled
template would otherwise leave the signing secret publicly known.
SECRET_ENCRYPTION_KEY is the one with no recovery path — see
Backup and restore.
WRIT_PUBLIC_URL is the load-bearing one. Everything else about reachability
is derived from it:
- agents dial it, and it is baked into the
/agent.shinstall one-liner; - its hostname is automatically trusted by the Host-header allowlist;
- absolute links in emails and OAuth metadata use it.
Required in production. Set it to the URL people actually open.
| Variable | Notes |
|---|---|
WRIT_PUBLIC_URL |
https://writ.example.com. Required in production. |
ALLOWED_HOSTS |
Extra hostnames only. You usually need none — the public URL's host and loopback are trusted for you. Supports *.suffix. |
CORS_ORIGINS |
Explicit origins; * is refused in production. The SPA is same-origin, so this only matters for API clients running in a browser. |
FORWARDED_ALLOW_IPS |
Which upstream may set X-Forwarded-For. Must name your proxy. |
ENVIRONMENT |
production (default) or development. |
FORWARDED_ALLOW_IPS is the one people get wrong. If your proxy's address is
not listed, every request appears to come from the proxy: all clients share a
single rate-limit bucket, one attacker's failed logins lock out everyone, and
audit logs record the proxy instead of the caller. With the bundled Caddy it is
127.0.0.1,172.16.0.0/12 (the container range); with your own proxy it is that
proxy's address. Never * on anything internet-reachable.
Read only by the bundled Caddy, under the tls compose profile.
./scripts/deploy.sh fills them in.
| Variable | Notes |
|---|---|
WRIT_DOMAIN |
the hostname to request a certificate for |
WRIT_ACME_EMAIL |
Let's Encrypt expiry / failure notices |
WRIT_ACME_CA |
directory URL. Never blank. --staging swaps it. |
WRIT_DOC_EXTRACT_DOMAIN |
optional second hostname for the extractor |
| Variable | Default | Notes |
|---|---|---|
GLOBAL_RATE_LIMIT_ENABLED |
true |
per-IP ceiling across the whole surface |
GLOBAL_RATE_LIMIT_REQUESTS |
600 |
per window |
GLOBAL_RATE_LIMIT_WINDOW |
60 |
seconds |
Deliberately generous — a DoS backstop, not a quota. One browser tab loading the app and polling already spends tens of requests a minute. Static assets, the healthcheck and CORS preflights are exempt. The paths that matter carry their own much stricter limits (login and password-reset brute-force lockout, single-use pairing codes, webhook caps).
| Variable | Default | Notes |
|---|---|---|
DOC_EXTRACT_URL |
http://127.0.0.1:8092 |
as your AGENTS see it, not the coordinator |
DOC_EXTRACT_SECRET |
generated | must match on both sides |
DOC_EXTRACT_OCR_MODE |
auto |
read the text layer when there is one; OCR only true scans |
DOC_EXTRACT_TIER |
light |
rich adds docling for complex tables, and pulls torch |
Set DOC_EXTRACT_URL= (empty) to disable the lane entirely.
| Variable | Default | Notes |
|---|---|---|
LOG_LEVEL |
info |
|
ENABLE_METRICS |
off | exposes unauthenticated Prometheus metrics at /metrics
|
REQUIRE_ADMIN_MFA |
false |
turn on after every admin has enrolled a factor |
ALLOW_PRIVATE_TARGETS |
false |
let crawls reach RFC1918 / loopback. Other SSRF screens stay on regardless. |
ALLOW_INSECURE_DEV |
false |
boot with the shipped default signing key. Development only, never on a network. |
-
One worker process. The JWT blacklist, rate limits, presence and the
scheduler live in in-process state over a single SQLite file. Extra workers
each get an empty keyspace, so token revocation silently stops working and the
scheduler double-fires. Setting
WEB_CONCURRENCY>1refuses to start. Scale browser work by connecting more agents instead. -
Storage paths. Pinned to the
/datavolume by compose.
usewrit/writ · AGPL-3.0-only · Issues · Discussions · Report a vulnerability
Getting started
Using it
Integrations
Operations
Reference