Configuration
Three sources: environment variables, a YAML config file, and runtime edits through the admin API or UI. Environment alone is enough to run; the file exists for configuration-as-code, and it's the only way to declare projects and users.
Server environment
| Variable | Default | Meaning |
|---|---|---|
PORT / FW_SERVER_PORT | 4317 | HTTP listen port |
FW_DATA_DIR | .flowwright | State root, resolved against the working directory |
FW_WORKERS | 2 | In-process worker pool. 0 delegates to standalone workers |
FW_LOG_LEVEL | info | debug · info · warn · error |
FW_AUTH | required | required or disabled |
FW_MFA_POLICY | optional | optional · required_privileged · required_all |
FW_REGISTRATION_MODE | closed | open lets any visitor create a member |
FW_SECRET_KEY | generated | Credential encryption key, 32 bytes as hex or base64 |
FW_WEBHOOK_SECRET | — | Shared secret for inbound provider webhooks |
FW_METRICS_TOKEN | — | Bearer token accepted by /api/metrics |
FW_DATABASE_URL | — | PostgreSQL connection string. Absent → SQLite |
FW_ARTIFACTS_S3_BUCKET | — | S3 bucket for artifacts. Absent → local filesystem |
FW_CONFIG_FILE | <dataDir>/flowwright.server.yaml | Config file path |
FW_RATE_LIMIT / FW_RATE_WINDOW_MS | — | Request rate limit and window |
FW_RETENTION_RUNS / FW_RETENTION_DAYS | — | Garbage-collection thresholds |
FW_ARTIFACT_QUOTA_BYTES | — | Artifact storage cap |
Worker environment
| Variable | Default | Meaning |
|---|---|---|
FW_DATA_DIR | .flowwright | Must reach the server's state |
FW_WORKERS / FW_CONCURRENCY | 2 | Stages this worker runs at once |
FW_WORKER_LABELS | — | Comma-separated, e.g. linux,gpu |
FW_WORKER_CAPABILITIES | — | Comma-separated, beyond auto-detected platform/arch |
FW_WORKER_NAME / FW_WORKER_TOKEN | — | Worker identity and its token |
FW_SERVER_URL | — | Control plane URL, for minting checkout tokens |
FW_POLICY_FILE | — | Path to a JSON policy |
FW_DOCKER | auto | auto · disabled · required |
FW_DOCKER_USER / _PULL / _CPUS / _MEMORY / _NETWORK / _HOST / _CONTEXT | — | Container execution options |
FW_KUBERNETES | disabled | auto · required · disabled |
FW_KUBERNETES_NAMESPACE | default | Namespace for Kubernetes execution |
FW_WORKERS and FW_CONCURRENCY both set worker concurrency. If both are present
FW_WORKERS wins, with no warning.
FW_DATABASE_URL and FW_ARTIFACTS_S3_BUCKET must match the server's.
The config file
<dataDir>/flowwright.server.yaml, or wherever FW_CONFIG_FILE points. YAML or JSON.
infra:
externalBaseUrl: https://ci.example.com
security:
authMode: required
secretKey: { fromEnv: FW_SECRET_KEY }
operational:
logLevel: info
concurrency: 4
retention:
keepPerProject: 50
maxAgeDays: 90
resources:
projects:
- name: web
repo: github.com/acme/web
branch: main
users:
- name: ezequiel
role: admin
A declared project takes every field the UI exposes, not just the three above:
| Key | Default |
|---|---|
name | required — and the identity, see below |
repo | required |
branch | the repository's default |
file | pipeline.ts |
setup | auto-detected from a lockfile |
schedule | none |
archived | left alone unless declared |
priority | 0 |
maxConcurrency | unlimited |
worker.labels | none |
worker.capabilities | none |
worker.preferredLabels | none |
Note worker.labels and worker.capabilities here are requiredLabels and
requiredCapabilities in the API — same fields, different names.
A project is identified by name, and an omitted key means "the default". Declaring a
project without setup doesn't leave setup alone; it clears whatever the UI set, on every
boot. Renaming a declared project in the UI makes the file create a second one. What each
field does is on Projects.
Secrets are never literals. Every secret position takes { fromEnv: NAME } or
{ fromFile: /path }, and a plain string fails validation — so the file is safe to
commit.
The worker has its own, flowwright.worker.yaml, with database, artifacts,
concurrency, labels, capabilities, policy, credentialProviders, docker,
kubernetes, network and identity.
Unknown keys are an error
A misspelled key used to be stripped in silence — the file parsed, the server booted, and the setting did nothing. Now it fails, naming every typo at once:
config: unknown config key(s): secuirty, operational.retention.maxAgeDay
Two blocks that older files sometimes carry are rejected for the same reason, and both
were already doing nothing: security.sourceControl (provider apps are registered
through the admin UI, not the file) and resources.projects[].secrets (project
credentials are created through the API).
security.credentialProviders is worker-only. Credentials are resolved by the worker,
just-in-time before the declaring stage, so the block belongs in
flowwright.worker.yaml. It is rejected in the server file rather than accepted and
ignored.
What wins
The file is authoritative for what it declares. Mechanically: values it declares are
written into the database stamped file, everything reads the database, and a key the
file stops declaring reverts to the environment default. A UI edit to a key the file
never mentions persists.
Two exceptions where the environment beats the file, both deliberate:
FW_DATABASE_URL and FW_ARTIFACTS_S3_BUCKET.
GET /api/config shows the effective value and its provenance — file, ui, or the
default — which is the only reliable way to answer "why is this set to that".
Reloading
$ kill -HUP <pid> # or: POST /api/admin/reload
Reload re-reads the file. A malformed file is non-fatal — the error is logged, the running configuration is untouched, and the API returns 400. A malformed file at boot is fatal.
What applies live: operational (log level, concurrency, rate limit, retention, UI
defaults), security.authMode / mfaPolicy / registrationMode,
infra.externalBaseUrl, scheduling, and resources.
What doesn't, and needs a restart:
infra.portsecurity.secretKey,webhookSecret,metricsTokennotifications.emailinfra.outboundProxydatabase,artifacts
Reload reports these in restartRequired rather than implying it applied them. It
used to return an empty list, which is how you could change SMTP settings, get
{"ok": true}, and have nothing happen.
The worker has no reload at all. Its config is boot-only; restart it.