- CLI Reference
- Configuration
Configuration
This page documents all environment variables and default settings for the Ironflow server and its SDKs.
Environment Variables
Section titled “Environment Variables”| Variable | Description | Default |
|---|---|---|
IRONFLOW_SERVER_URL | The URL SDKs and CLI use to connect. IRONFLOW_URL is an alias checked first by the Go agent SDK, the JS serve() push handler, and @ironflow/node/agent. createWorker and createClient ignore it. JS webhook emit inside serve() reads IRONFLOW_URL only, never IRONFLOW_SERVER_URL. | http://localhost:9123 |
IRONFLOW_API_KEY | The key used for authentication. | (None) |
IRONFLOW_DATABASE_URL | Postgres connection string (triggers PG mode). | (None, uses SQLite) |
IRONFLOW_BLOB_URL | Blob overflow backend for large run/step outputs. Empty uses local filesystem next to the SQLite DB; s3://bucket?endpoint=...®ion=... selects an S3-compatible backend. Multi-node deployments require an s3:// value. See Artifact overflow to blob storage. | (None, local filesystem) |
IRONFLOW_ARTIFACT_THRESHOLD | Size above which run/step outputs offload to the blob backend (1MB, 512KiB, etc.). Empty or 0 disables new offloads; reads still inflate existing blobs. | (None, off) |
AWS_ACCESS_KEY_ID | Access key used by the S3-compatible blob backend when IRONFLOW_BLOB_URL is set. | (None) |
AWS_SECRET_ACCESS_KEY | Secret key used by the S3-compatible blob backend when IRONFLOW_BLOB_URL is set. | (None) |
AWS_SESSION_TOKEN | Optional session token for temporary S3 credentials. | (None) |
IRONFLOW_MASTER_KEY | 32-byte hex key for secrets encryption. | (None, Dev Mode) |
IRONFLOW_JWT_SECRET | 32-byte hex key for dashboard JWT signing. Auto-generated and persisted to .ironflow_jwt_secret if not set. | (Auto-generated) |
IRONFLOW_SIGNING_KEY | Key for verifying push-mode webhooks. | (None) |
IRONFLOW_WEBHOOK_SECRET_GRACE_HOURS_DEFAULT | Default grace window (integer hours) for webhook secret rotation. Values above the 7-day cap or unparseable values are fatal at boot. | 24 |
IRONFLOW_ENV | Default environment for CLI and SDK operations. | default |
IRONFLOW_CONFIG_DIR | Directory for storing platform credentials (credentials.json). Used by ironflow platform login. | ~/.config/ironflow |
IRONFLOW_MCP_BEARER_TOKEN | Bearer token required by ironflow mcp --transport=streamable-http. Required for that transport; the server refuses to start without it and never accepts a non-loopback host. Unused by the default stdio transport. | (None) |
NATS_STORE_DIR | Disk path for embedded NATS JetStream storage. | (Auto-derived from --db path: {db_dir}/ironflow-nats. See Embedded NATS storage below.) |
LOG_LEVEL | trace, debug, info, warn, error. Overrides per-command defaults. | warn (CLI), info (serve) |
LOG_FORMAT | text for human-readable output. Only affects serve command (JSON by default). CLI commands always use text. | text (CLI), JSON (serve) |
HCLOUD_TOKEN | Hetzner Cloud API token used by ironflow provision. | (None) |
KUBECONFIG | Kubeconfig path used by ironflow deploy. The --kubeconfig flag wins when non-empty. A leading ~ is expanded, because fish’s export KUBECONFIG=~/... does not expand it. | (None) |
HETZNER_S3_ENDPOINT | Auto-injected by ironflow deploy as Helm postgresql.objectStore.endpointURL when not already supplied via --set. | (None) |
HETZNER_S3_BUCKET | Auto-injected by ironflow deploy as Helm postgresql.objectStore.destinationPath (wrapped as s3://{bucket}/backups) when not already supplied via --set. | (None) |
S3 Usage at a Glance
Section titled “S3 Usage at a Glance”Ironflow uses S3-compatible storage in a few different places. These settings are intentionally separate because they protect different data.
| Surface | What goes to S3 | Dev / single-node default | Production setting | Configure with |
|---|---|---|---|---|
| Runtime blob overflow | Large run and step outputs after they exceed the configured threshold. Reads inflate the payload transparently. | Local filesystem next to the SQLite DB; offload is disabled until a threshold is set. | Multi-node deployments require a shared s3:// backend, even when new offloads are disabled. | IRONFLOW_ARTIFACT_THRESHOLD, IRONFLOW_BLOB_URL, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, optional AWS_SESSION_TOKEN; YAML: spec.blobs.artifactThreshold, spec.blobs.url. See Artifact overflow to blob storage. |
| Kubernetes PostgreSQL backups | CNPG/Barman WAL archives and database backups. | Disabled unless the Helm object store settings are enabled. | Enable postgresql.objectStore and point it at an S3-compatible bucket. | Helm values: postgresql.objectStore.enabled, destinationPath, endpointURL, retentionPolicy, s3Credentials. See Kubernetes deployment. |
| Cloud operator break-glass and snapshots | Encrypted break-glass payloads and deprovision snapshot exports for managed-cloud workflows. | Not used by normal ironflow serve; only used by ironflow-cloud workflows. | Set the cloud operator S3 endpoint, bucket, credentials, region, and TLS flag. | IRONFLOW_CLOUD_BREAK_GLASS_ENDPOINT, IRONFLOW_CLOUD_BREAK_GLASS_REGION, IRONFLOW_CLOUD_BREAK_GLASS_BUCKET, IRONFLOW_CLOUD_BREAK_GLASS_ACCESS_KEY, IRONFLOW_CLOUD_BREAK_GLASS_SECRET_KEY, IRONFLOW_CLOUD_BREAK_GLASS_SECURE. |
The broader object/file storage API described in ADR-0035 is separate design
work and is not configured by IRONFLOW_STORAGE_* variables in the shipped
server path.
Embedded NATS storage
Section titled “Embedded NATS storage”ironflow serve runs an embedded NATS JetStream server when NATS_URL is not
set. JetStream needs a storage directory for streams and consumer state.
Default behavior (since #614): the store directory is auto-derived from the
SQLite database path. For the default .ironflow/ironflow.db it becomes .ironflow/ironflow-nats/;
for /data/ironflow.db it becomes /data/ironflow-nats/. The directory is
created at startup with 0700 permissions (owner read/write/exec only) because
JetStream stream files contain workflow event payloads that may include PII or
secrets. Existing directories are left as-is so operators can pre-create with
their preferred ownership and permissions.
Memory-mode opt-out: pass an explicit empty value for testing, demos, or ephemeral CI:
ironflow serve --nats-store-dir=""The explicit empty CLI flag wins over a NATS_STORE_DIR environment variable
inherited from the shell or systemd unit.
YAML-only opt-out is not currently supported. YAML round-trip cannot
distinguish nats.storeDir: "" from an omitted field, so a YAML config alone
cannot request memory mode. YAML operators who want ephemeral embedded NATS
have two options today: (1) point at an external NATS server via nats.url,
or (2) override at startup with --nats-store-dir="" on the command line. A
discoverable :memory: sentinel for --nats-store-dir and NATS_STORE_DIR
is tracked in TODOS.md as a follow-up.
Postgres mode: when IRONFLOW_DATABASE_URL is set and NATS_URL is NOT set
(embedded NATS), ironflow serve and ironflow validate refuse to start
without an explicit --nats-store-dir or NATS_STORE_DIR. Auto-deriving from
the SQLite path is not safe — there is no SQLite path in Postgres mode, and
silently creating a directory in the current working directory is dangerous
under systemd units, container WorkingDirectory: /, and read-only cwd. Set
the directory explicitly when you want embedded NATS with Postgres, or use
--nats-url for an external NATS server.
:memory: SQLite: same fail-fast as Postgres mode. --db :memory: plus
embedded NATS without an explicit store dir is rejected at startup.
Security note — secrets KV plaintext on disk. The secrets KV bucket is
backed by JetStream file storage. When IRONFLOW_MASTER_KEY is unset, secret
values are stored without encryption (passthrough). Before #614 this was
masked by memory-mode embedded NATS; with persistent NATS as the default,
secrets without a master key persist plaintext to {store_dir}/secrets/....
The server emits a startup WARN when this configuration is detected and a
stronger WARN with the count of plaintext secrets when any exist. Set
IRONFLOW_MASTER_KEY to a 32-byte hex value (e.g. openssl rand -hex 32) for
AES-256-GCM encryption, then re-set existing secrets via ironflow secret set
so they get re-encrypted.
Storage profile. File-storage streams retain about 1.7 GB total
(internal/nats/streams.go), versus about 75 MB total in memory mode.
Operators on size-constrained disks can override per-stream limits via YAML
or roll back to memory mode with --nats-store-dir="".
Operator discipline. The embedded NATS server does not lock its store
directory, so two ironflow serve processes against the same DB directory
will silently corrupt JetStream state. Run a single process per directory.
Observability
Section titled “Observability”| Variable | Description | Default |
|---|---|---|
IRONFLOW_OTEL_ENDPOINT | OTLP gRPC endpoint for OpenTelemetry tracing. | (None, disabled) |
IRONFLOW_OTEL_SAMPLE_RATE | Trace sampling rate (0.0 to 1.0). | 1.0 |
IRONFLOW_OTEL_SERVICE_NAME | OTel service name. | ironflow |
IRONFLOW_OTEL_INSECURE | Use plaintext gRPC for OTLP export. | true |
IRONFLOW_METRICS_ENABLED | Enable Prometheus metrics at /metrics. | false |
Profiling
Section titled “Profiling”The --pprof CLI flag on ironflow serve starts a separate listener on :6060 with Go’s net/http/pprof handlers for CPU, heap, and goroutine profiling. Not configured via environment variable — CLI flag only. See Benchmarks for the load testing workflow.
Clustering
Section titled “Clustering”| Variable | Description | Default |
|---|---|---|
NATS_URL | External NATS server URL. When set, connects to external NATS instead of embedded. Requires PostgreSQL. | (None, embedded) |
NATS_CREDS_FILE | NATS credentials file for NKey/JWT auth. | (None) |
IRONFLOW_NODE_ID | Unique node identifier for distributed coordination. Must be stable per node. | Random UUID v4 |
IRONFLOW_STALE_CLAIM_THRESHOLD | Duration before stale scheduler claims are reclaimed. Covers orphaned sleep/wait wake-ups only — it does not control how fast a crashed worker’s run is recovered. That path is gated on concurrency-lease expiry, which is not configurable. See Crash recovery. | 2m |
IRONFLOW_STALE_CLAIM_RECOVERY_INTERVAL | How often the scheduler scans for orphaned claims. Lower values detect crashes faster at the cost of more frequent SQL sweeps. The --dev flag drops it to 5s. | 60s |
IRONFLOW_CLUSTER_ID | Cluster identifier attached to cluster-scoped audit emits (e.g. cluster.token.rotated) so multi-cluster deployments can attribute rows. | (None, empty in dev) |
NATS_FILE_STORAGE | Set to true to use file-backed JetStream streams instead of memory. | false |
NATS_STREAM_REPLICAS | JetStream stream replica count for clustered NATS. | 1 |
Multi-node clustering requires PostgreSQL. SQLite is single-node only — it supports
crash-resume for a restarted process but not multi-node coordination (#613). Every
variable in this table except IRONFLOW_CLUSTER_ID is on the flags-only list: with
-f file.yaml set them as spec.nats.* / spec.cluster.* fields instead.
Security
Section titled “Security”| Variable | Description | Default |
|---|---|---|
IRONFLOW_WS_ALLOWED_ORIGINS | Comma-separated allowed origins for WebSocket connections. When empty, all origins are allowed. | (None, all origins) |
IRONFLOW_AGENT_TOOLS_ALLOW_PRIVATE | When true, agent tool callback URLs may target RFC1918/loopback addresses. The --dev flag forces this on regardless. | false |
IRONFLOW_AGENT_TOOLS_URL_ALLOWLIST | Comma-separated host allowlist for agent tool callback URLs. When set, only listed hosts are accepted. | (None, no allowlist) |
IRONFLOW_REQUIRE_ENCRYPTION | When true/1/yes, the engine verifies the SQLite data path lives on a dm-crypt block device and fails fast at startup otherwise. Postgres backends skip the check. Belt-and-suspenders against misconfigured systemd BindsTo units. | false (off) |
Engine
Section titled “Engine”| Variable | Description | Default |
|---|---|---|
IRONFLOW_CANCEL_ON_REPLAY_WINDOW_SECONDS | Lookback window (seconds) for the cancel-on-replay buffer. Recent events newer than this are kept for cancellation matching. | 30 |
IRONFLOW_CANCEL_ON_REPLAY_BUFFER_SIZE | Maximum entries retained in the cancel-on-replay buffer. Bounds memory under high event rates. | 10000 |
IRONFLOW_SQLITE_MAX_BYTES | Caps total SQLite footprint (main + WAL + SHM). At the cap, REST writes return 507 Insufficient Storage; unary RPC mutations return failed_precondition with Retry-After: 30. Deliberately not unavailable — the SDKs classify that retryable, and a full disk is permanent until an operator acts. SQLite backends only. Off unless set (#2113). | 0 (disabled) |
IRONFLOW_WRITE_QPS_MAX | Caps REST writes and unary RPC mutations per second per node. Excess writes return 429 / resource_exhausted with Retry-After: 1. Applies on every backend, PostgreSQL included — unlike the disk cap above, which is SQLite-only. One bucket per node, not per cluster. Off unless set (#2113). | 0 (disabled) |
Cloud Operator
Section titled “Cloud Operator”| Variable | Description | Default |
|---|---|---|
IRONFLOW_CAP_TOKEN_PRIVATE_KEY_B64 | Base64-encoded PKCS8 DER Ed25519 private key for minting deprovision capability tokens (ironflow cloud cap-token mint). | (None, required for mint) |
IRONFLOW_MASTER_KEY_B64 | Base64-encoded 32-byte meta-cluster KEK for decrypting break-glass bearer blobs and snapshot exports (ironflow cloud break-glass, ironflow cloud snapshot decrypt). | (None, required for decrypt) |
Auth Audit Batcher
Section titled “Auth Audit Batcher”| Variable | Description | Default |
|---|---|---|
IRONFLOW_AUDIT_BATCH_SIZE | Maximum rows per batch insert into the policy_decisions audit table. | 100 |
IRONFLOW_AUDIT_BATCH_FLUSH_MS | Milliseconds between batch flushes. Set lower for tighter audit latency; set higher for write throughput. | 1000 |
IRONFLOW_AUDIT_KV_EMIT | Set to true/1/yes/on to record KV bucket + key CRUD in the audit table. Off by default so hot-path KV writes do not bloat the audit table. | false |
IRONFLOW_AUDIT_RETENTION_DAYS | Days of audit rows kept by the nightly pruner. 0 disables pruning; values between 1 and 6 are rejected at startup (7-day safety floor). | 90 |
IRONFLOW_AUDIT_PAYLOAD_REDACT_FIELDS | Comma-separated exact <event_type>.<field> selectors from the workflow payload catalog. Values are trimmed and sorted. Unknown, malformed, duplicate, nested, wildcard, and protected-family selectors fail startup. Changes require a restart. | (None, full payload capture) |
Event Schema Enforcement
Section titled “Event Schema Enforcement”| Variable | Description | Default |
|---|---|---|
IRONFLOW_EVENT_SCHEMA_ENFORCEMENT | Whether emitted event payloads are validated against the event schema registry. off performs no lookup; warn logs a mismatch and accepts the event; reject refuses it with 400 and writes nothing. In reject, a failed registry read refuses the event with 503 rather than accepting it unvalidated — see enforcement failure modes. An unrecognized value refuses to start the server. | off |
Enforcement is off by default because warn cannot short-circuit — it must read
the registry to know whether to warn — so any other default would add a
synchronous query to every emit, including on the many deployments that have
never registered a schema. Nothing registers one automatically.
Validation applies to events entering through the HTTP and RPC emit paths, whose request context carries an environment.
It does not apply to engine-generated events (cron ticks,
ironflow/function.invoked), which never carry a user-registered schema.
It does apply to the NATS ingest stream (#1986). Registry lookups are
environment-scoped, and that path runs on the server’s startup context with no
environment attached, so the environment is resolved per message from the
subject instead: the EVENTS stream subject is
ironflow.{projectName}.{envName}.events.>, and the pair it carries is looked
up against the environment and project tables. A rejected event is TERMed, not
redelivered.
The subject carries names, and environment names are unique per project while
project names are unique per organization — so the pair identifies one
environment only within an organization. If it matches zero environments,
or matches more than one because two organizations use the same project name,
the message is accepted unvalidated. Validating one tenant’s payload
against another tenant’s schema would be worse than not validating: in reject
it is a denial with no retry.
A resolved pair is cached for the life of the process, so renaming an environment (or deleting one and reusing its name in another organization) needs a restart to take effect on this path. A pair that does not resolve is re-checked after 60 seconds, so an environment created after a publisher already started using its subject begins being enforced on its own.
Registering a schema does not fetch anything: external $ref resolution is
disabled, so a schema may only reference definitions inside its own document.
A payload is validated against the schema registered for its exact
(event_name, version, environment), never the latest version — so registering
a new version cannot retroactively invalidate emitters that have not migrated.
If an event name has no schema registered at any version, it passes freely:
that is what keeps enforcement inert for anyone who has registered nothing. But
once any version is registered, the name is governed, and an event carrying a
version with no schema is rejected rather than accepted. Without that rule
the version field — which is client-supplied on every path that carries one,
and which trigger matching never consults — would be a way to walk straight
past a registered schema.
Every emit path can now select a version (#1955). POST /ironflow.v1.IronflowService/Emit takes
an optional version field, TriggerSync takes one on both transports, and
webhook ingest reads it from the source’s schema_version — a third-party
sender cannot express an Ironflow schema version, so it is the operator’s
choice at the source rather than the payload’s. Omitting it anywhere means
version 1. One version covers every event name a webhook source emits.
Deleting the last registered version of a governed event name is a breaking
change. Because matching is exact and a governed name rejects unregistered
versions, registering order.placed v2 and then deleting v1 turns every emit
still carrying v1 into a 400 — on every emit path, with no way for the emitter
to fix it. Register the new version, migrate emitters onto it, and only then
delete the old one.
If a stored schema does not compile, the event is accepted and the failure is logged: schemas registered before this check existed were never parsed.
Projection Rebuild
Section titled “Projection Rebuild”| Variable | Description | Default |
|---|---|---|
IRONFLOW_REBUILD_BATCH_SIZE_SQLITE | Events per batch when rebuilding a projection against a SQLite-backed event store. | 500 |
IRONFLOW_REBUILD_BATCH_SIZE_POSTGRES | Events per batch when rebuilding a projection against a PostgreSQL-backed event store. | 1000 |
IRONFLOW_REBUILD_BATCH_PAUSE_MS | Milliseconds to pause between FULL rebuild batches (PG only). Useful for throttling large rebuilds. | 0 |
Projection Catch-Up Waits
Section titled “Projection Catch-Up Waits”wait-for-catchup calls hold a slot for the life of the wait. Two limits guard
them: a fixed node-wide ceiling of 2000, and a per-organization slice of it so
one tenant filling its share still leaves the rest of the node available to
everyone else. Over either limit the call returns 429 Too Many Requests.
Raise the per-organization limit on a single-tenant deployment that
legitimately runs more than a quarter of the ceiling in concurrent waits;
setting it to 2000 restores a node-wide-only limit. Values above the node
ceiling are clamped to it. Do not set it below 16: a batch wait reserves
one slot per item and rejects the whole batch if it cannot get them all, so
a limit under the 16-item batch maximum fails full batches regardless of load.
| Variable | Description | Default |
|---|---|---|
IRONFLOW_PROJECTION_WAIT_CAP_PER_TENANT | Per-organization ceiling on concurrent projection catch-up waits. Capped at the node-wide ceiling of 2000. | 500 |
Bootstrap Defaults
Section titled “Bootstrap Defaults”On the very first boot of a fresh database, Ironflow creates a default admin user with the email admin@ironflow.local and a randomly generated password. Both the admin API key and password are printed to the console and must be saved immediately — they will not be shown again.
SDK Configuration
Section titled “SDK Configuration”TypeScript (Browser)
Section titled “TypeScript (Browser)”import { ironflow } from "@ironflow/browser";
ironflow.configure({ serverUrl: "http://localhost:9123", auth: { apiKey: "ifkey_..." }, environment: "default",});Additional options: transport (“connectrpc” | “websocket”), reconnect, visibility, logger, timeout.
TypeScript (Node.js)
Section titled “TypeScript (Node.js)”import { createClient } from "@ironflow/node";
const client = createClient({ serverUrl: process.env.IRONFLOW_SERVER_URL || "http://localhost:9123", apiKey: process.env.IRONFLOW_API_KEY,});Additional options: timeout (ms, default 30000), onError (global error handler).
API Payload Limits
Section titled “API Payload Limits”| Limit | Value | Applies To |
|---|---|---|
| Max Event Data | 512 KB | Payload of an emitted event. |
| Max Step Output | 1 MB | Data returned by a step.run. |
| Max Batch Size | 100 | Max events in a TriggerBatch call. |
| Wait Timeout | 30s | Default timeout for emit --wait. |
Event schema enforcement failure modes
Section titled “Event schema enforcement failure modes”IRONFLOW_EVENT_SCHEMA_ENFORCEMENT=reject treats two kinds of failure
differently, because they have different owners.
| What failed | Owner | warn | reject |
|---|---|---|---|
| Payload does not match the registered schema | The emitter | Logs, accepts | 400, nothing written |
| Event name has no registered schema | Nobody — the name is ungoverned | Accepts | Accepts |
| Registered schema will not compile | The operator who registered it | Logs, accepts | Logs, accepts |
| Registry read failed (store down, timeout, lock contention) | Infrastructure | Logs, accepts | 503, nothing written |
A schema that will not compile fails open on purpose: refusing a caller’s valid payload because a stored row is malformed puts the blame in the wrong place, and it would turn one bad registration into a total ingest outage.
A registry read that fails is the opposite case and fails closed in
reject. An operator who set reject asked for a guarantee, and a database
brownout must not quietly turn it off during exactly the incident that makes it
matter. The cost is real and worth stating: on SQLite, which runs with
SetMaxOpenConns(1), store contention under load that previously degraded
enforcement now refuses ingest with 503. The status is retryable by design —
emitters should back off, and the NATS ingest path redelivers rather than
dead-lettering.
An unrecognized value for the variable refuses to start the server rather than
falling back to off. Silently running unenforced because of a typo is the
worst of the available outcomes.