Skip to main content

Server

The control plane: HTTP API, web UI, run queue, and the system of record. One per deployment. This page is the daemon; using the web app it serves is its own section.

$ docker run -d -p 4317:4317 -v flowwright-data:/data flowwrightdev/flowwright

The request pipeline

request → rate limit → authentication → permission → controller → domain service

Auth and permissions are covered on Authentication. The rate limit is FW_RATE_LIMIT requests per FW_RATE_WINDOW_MS, or off.

The web UI is served from the same port as the API. If the UI assets are missing the server still answers /api/health — a green health check does not prove the UI shipped.

Stores

Two independent choices, each one environment variable.

State — runs, projects, users, the queue:

Selected byWhere
SQLitedefault<dataDir>/state.db
PostgreSQLFW_DATABASE_URL setYour server

Artifacts, logs and cache:

Selected byWhere
Filesystemdefault<dataDir>/runs
S3FW_ARTIFACTS_S3_BUCKET setYour bucket

They're orthogonal — Postgres with filesystem artifacts is valid, if every worker shares the filesystem. The reason to move both is to stop needing a shared filesystem at all.

Both settings must match on the server and every worker. They're also the two keys where the environment beats the config file.

In-process workers

FW_WORKERS (default 2) runs that many workers inside the server process. Fine for a single-host deployment and simpler than a second container.

Set it to 0 to make the server enqueue-only and run workers separately — what the compose file does, and what lets you scale execution without scaling the API.

Admin endpoints

All require settings:admin.

EndpointDoes
POST /api/admin/reloadRe-read the config file — same as SIGHUP
GET /api/admin/backupDownload a SQLite snapshot
POST /api/admin/gcRun retention garbage collection now

Plus GET /api/metrics, which accepts either an authenticated principal or Authorization: Bearer $FW_METRICS_TOKEN — so a scraper needs a token, not an account.

Health

GET /healthz and GET /api/health are the same handler and both public — no credentials.

{
"status": "ok",
"uptimeMs": 1500,
"schemaVersion": 49,
"serverProtocol": 1,
"version": "0.1.0",
"checks": { "store": "ok" }
}

The check is a cheap store read. A store that's unreachable gives 503 and "status": "degraded". It says nothing about workers, queue depth or disk — see Operations.

Behind a proxy

Terminate TLS in front and set infra.externalBaseUrl. Webhook and OAuth callback URLs — the ones you paste into a GitHub App or GitLab OAuth app — are derived from that value and never from request headers, so a wrong value produces callbacks pointing at the wrong host rather than a header-spoofing hazard.

The session cookie's Secure flag follows the forwarded protocol, so the proxy needs to send x-forwarded-proto.