Press / to search

All documentation
docs Run it somewhere Self-hosting

Runs onSelf-hosted

selfhost ▸ your server

Your server, your coordinator.

The open-core coordinator runs on hardware you own. The only requirement is Docker — no external database, cache or queue to operate, and no feature gates inside what ships.

AGPL-3.0-only. The source offer is served by your own install, at /api/about.

install ▸ three commands

Stand it up.

install.sh

git clone https://github.com/usewrit/writ.git && cd writ
./scripts/gen-env.sh
docker compose up -d --build

Then open http://localhost:8000 and create your account. The first build is slow because the document and OCR runtime is baked in, so extraction works offline from then on.

The coordinator launches no browsers itself. Nothing executes until you connect an agent — that is the whole design, not a limitation of the free tier.

Connect the first agent

In the app: Fleet → Connect a new agent, which mints a pairing code. Then, on the machine that will do the work:

connect-agent.sh

curl -fsSL http://localhost:8000/agent.sh | sh -s -- WRIT-4K2P-9XQ

Pairing codes are single-use and expire in 15 minutes. The agent dials outbound to the coordinator, so there are no inbound ports to open on the machine.

domain ▸ one command

Public domain and HTTPS.

One command takes an install from localhost to a real hostname with a real certificate:

deploy.sh

./scripts/deploy.sh writ.example.com you@example.com
  1. Checks Docker, the ports, and that your domain already points at this server.
  2. Writes every domain-derived setting into the env file, consistently.
  3. Brings up the bundled reverse proxy with automatic Let’s Encrypt certificates.
  4. Waits for the certificate and verifies the live HTTPS URL.

It is safe to re-run: use it to change the domain, to repair a half-finished deploy, or after a docker compose down. Existing secrets are never rotated.

Day-to-day

Ordinary Docker Compose, because that is all it is. Add --profile tls to any of these if you deployed with the reverse proxy:

operate.sh

docker compose logs -f
docker compose restart
docker compose down

# Removes the data volumes too — this deletes your data.
docker compose down -v

# Add --profile tls to any of these if you deployed with the reverse proxy.
docker compose --profile tls logs -f

secrets ▸ eight required

Secrets the stack will not start without.

./scripts/gen-env.sh generates all eight for you. In production the stack refuses to start if any one of them is missing, blank, or shorter than 32 characters:

WRIT_JWT_SECRET
API_SECRET_KEY
HMAC_SECRET_KEY
RECORDER_AUTH_SECRET
INTERNAL_API_SECRET
GATEWAY_SECRET
DOC_EXTRACT_SECRET
SECRET_ENCRYPTION_KEY

Back up SECRET_ENCRYPTION_KEY somewhere separate from your data volume. It is the key your stored secrets are encrypted with. Lose it and those secrets cannot be recovered — not by you, and not by anyone else.

Production settings

Four settings carry real weight once you are past localhost:

SettingWhy it matters
WRIT_PUBLIC_URLLoad-bearing: agents dial it and the install script embeds it. Production refuses to start without it.
ENVIRONMENT=productionKeep it. It is what enforces the secret strength and the rules below.
CORS_ORIGINSSet explicit origins. A wildcard is refused in production.
FORWARDED_ALLOW_IPSSet it when you run behind your own proxy, so client addresses are read correctly.

runtime ▸ two containers

What actually runs.

Two containers, plus an optional reverse proxy. There is no external database, cache or queue for you to operate, back up or upgrade.

ContainerPortWhat it is
Coordinator8000The API, the web app, and its own embedded database.
Document extraction8092Document and OCR extraction, bundled so it works offline.
Reverse proxy80 · 443Optional, under the tls compose profile. Automatic certificates.

Both services expose a health endpoint, so your own monitoring has something to poll.

account ▸ single owner

The first account.

Set the admin email, password and name in the env file before first boot — or leave the password blank and finish setup in the browser, with the email pre-filled on the setup page.

WRIT_ADMIN_EMAILThe owner’s email, or just the address pre-filled on the setup page.
WRIT_ADMIN_PASSWORDLeave blank to complete setup in the browser instead.
WRIT_ADMIN_NAMEDisplay name for the owner account.

This build is single-owner: once an account exists, a second registration attempt is refused with 403. There is no email-based password reset — recover with the bundled reset script, which you run on the server itself.

mcp ▸ writ-selfhost

Point an assistant at your own coordinator.

Your coordinator registers as writ-selfhost, on purpose — so it coexists with the Writ desktop app, which registers as writ, and with Writ Cloud, which registers as writ-cloud.

connect-mcp.sh

claude mcp add writ-selfhost -e WRIT_API_KEY=<YOUR_API_KEY> \
  -- npx -y writ-mcp --url http://localhost:8000

The API key must carry the mcp:execute scope, or every call is refused. Add the resource scopes you want the assistant to have alongside it.

scope ▸ in and out

What is in the box, and what is not.

Included, with no feature gates

All of this ships in the self-hosted build and is not restricted by any plan:

  • Recording and step editing
  • The AI assistant in the recorder, using your own provider key
  • Personas with TOTP and OTP, and warm sessions
  • Running and scheduling
  • Monitors — availability, certificates and change history
  • Automations
  • Distributed crawl across your own fleet
  • Document and OCR extraction
  • Publishing REST endpoints
  • MCP tools
  • An OpenAI-compatible chat surface
  • Datasets, with export

Not in the self-hosted build

These are surfaces Writ operates rather than ships. They are absent, not degraded:

  • The marketplace
  • Billing and plan enforcement
  • Multi-tenant organizations
  • Managed AI — self-host is bring-your-own-key only
  • Managed residential egress
  • Provisioned cloud agents and autoscaling
  • SSO and SCIM
  • Scribe

upgrade ▸ license

Upgrades and license.

To upgrade, pull the repository and rebuild — the same three commands you started with, minus the clone.

Licensed AGPL-3.0-only, except the MCP connector (MIT) and the bundled fonts (SIL OFL 1.1). Because this is a network copyleft license, your install serves its own source offer at /api/about, linked from the login screen and from settings.

faq

Self-hosting questions, answered.

What do I need on the server?
Docker, and nothing else. There is no external database, cache or queue to install or operate — the coordinator carries its own embedded database, and the document-extraction runtime is baked into the image so extraction works offline.
Why does nothing run after I bring the stack up?
Because the coordinator launches no browsers itself. Connect an agent from Fleet → Connect a new agent, then run the install line on the machine that will do the work. Pairing codes are single-use and expire in 15 minutes.
What happens if I lose SECRET_ENCRYPTION_KEY?
Your stored secrets cannot be recovered. It is the key they are encrypted with, so back it up somewhere separate from the data volume — a backup that contains both is one failure away from being useless.
How do I put it on a real domain with HTTPS?
./scripts/deploy.sh writ.example.com you@example.com. It checks DNS and ports, writes the env file, brings up the bundled reverse proxy with automatic Let’s Encrypt certificates, and verifies the live URL. It is safe to re-run and never rotates existing secrets.
Can I add a second user?
Not in this build — it is single-owner, and a second registration attempt is refused with 403. Multi-tenant organizations, SSO and SCIM are cloud-operated surfaces and are absent here.
I forgot the admin password and there is no reset email.
Correct — there is no email-based password reset in the self-hosted build. Recover with the bundled reset script, run on the server itself. Since you already have server access, that is the same trust boundary.

end ▸ deploy

Clone it and bring it up.

Three commands to a working coordinator, one more to put it on your own domain.