Skip to content

Configuration

This page documents all environment variables and default settings for the Ironflow server and its SDKs.


VariableDescriptionDefault
IRONFLOW_SERVER_URLThe 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_KEYThe key used for authentication.(None)
IRONFLOW_DATABASE_URLPostgres connection string (triggers PG mode).(None, uses SQLite)
IRONFLOW_BLOB_URLBlob overflow backend for large run/step outputs. Empty uses local filesystem next to the SQLite DB; s3://bucket?endpoint=...&region=... selects an S3-compatible backend. Multi-node deployments require an s3:// value. See Artifact overflow to blob storage.(None, local filesystem)
IRONFLOW_ARTIFACT_THRESHOLDSize 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_IDAccess key used by the S3-compatible blob backend when IRONFLOW_BLOB_URL is set.(None)
AWS_SECRET_ACCESS_KEYSecret key used by the S3-compatible blob backend when IRONFLOW_BLOB_URL is set.(None)
AWS_SESSION_TOKENOptional session token for temporary S3 credentials.(None)
IRONFLOW_MASTER_KEY32-byte hex key for secrets encryption.(None, Dev Mode)
IRONFLOW_JWT_SECRET32-byte hex key for dashboard JWT signing. Auto-generated and persisted to .ironflow_jwt_secret if not set.(Auto-generated)
IRONFLOW_SIGNING_KEYKey for verifying push-mode webhooks.(None)
IRONFLOW_WEBHOOK_SECRET_GRACE_HOURS_DEFAULTDefault grace window (integer hours) for webhook secret rotation. Values above the 7-day cap or unparseable values are fatal at boot.24
IRONFLOW_ENVDefault environment for CLI and SDK operations.default
IRONFLOW_CONFIG_DIRDirectory for storing platform credentials (credentials.json). Used by ironflow platform login.~/.config/ironflow
IRONFLOW_MCP_BEARER_TOKENBearer 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_DIRDisk path for embedded NATS JetStream storage.(Auto-derived from --db path: {db_dir}/ironflow-nats. See Embedded NATS storage below.)
LOG_LEVELtrace, debug, info, warn, error. Overrides per-command defaults.warn (CLI), info (serve)
LOG_FORMATtext for human-readable output. Only affects serve command (JSON by default). CLI commands always use text.text (CLI), JSON (serve)
HCLOUD_TOKENHetzner Cloud API token used by ironflow provision.(None)
KUBECONFIGKubeconfig 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_ENDPOINTAuto-injected by ironflow deploy as Helm postgresql.objectStore.endpointURL when not already supplied via --set.(None)
HETZNER_S3_BUCKETAuto-injected by ironflow deploy as Helm postgresql.objectStore.destinationPath (wrapped as s3://{bucket}/backups) when not already supplied via --set.(None)

Ironflow uses S3-compatible storage in a few different places. These settings are intentionally separate because they protect different data.

SurfaceWhat goes to S3Dev / single-node defaultProduction settingConfigure with
Runtime blob overflowLarge 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 backupsCNPG/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 snapshotsEncrypted 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.

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:

Terminal window
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.

VariableDescriptionDefault
IRONFLOW_OTEL_ENDPOINTOTLP gRPC endpoint for OpenTelemetry tracing.(None, disabled)
IRONFLOW_OTEL_SAMPLE_RATETrace sampling rate (0.0 to 1.0).1.0
IRONFLOW_OTEL_SERVICE_NAMEOTel service name.ironflow
IRONFLOW_OTEL_INSECUREUse plaintext gRPC for OTLP export.true
IRONFLOW_METRICS_ENABLEDEnable Prometheus metrics at /metrics.false

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.

VariableDescriptionDefault
NATS_URLExternal NATS server URL. When set, connects to external NATS instead of embedded. Requires PostgreSQL.(None, embedded)
NATS_CREDS_FILENATS credentials file for NKey/JWT auth.(None)
IRONFLOW_NODE_IDUnique node identifier for distributed coordination. Must be stable per node.Random UUID v4
IRONFLOW_STALE_CLAIM_THRESHOLDDuration 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_INTERVALHow 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_IDCluster 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_STORAGESet to true to use file-backed JetStream streams instead of memory.false
NATS_STREAM_REPLICASJetStream 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.

VariableDescriptionDefault
IRONFLOW_WS_ALLOWED_ORIGINSComma-separated allowed origins for WebSocket connections. When empty, all origins are allowed.(None, all origins)
IRONFLOW_AGENT_TOOLS_ALLOW_PRIVATEWhen true, agent tool callback URLs may target RFC1918/loopback addresses. The --dev flag forces this on regardless.false
IRONFLOW_AGENT_TOOLS_URL_ALLOWLISTComma-separated host allowlist for agent tool callback URLs. When set, only listed hosts are accepted.(None, no allowlist)
IRONFLOW_REQUIRE_ENCRYPTIONWhen 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)
VariableDescriptionDefault
IRONFLOW_CANCEL_ON_REPLAY_WINDOW_SECONDSLookback window (seconds) for the cancel-on-replay buffer. Recent events newer than this are kept for cancellation matching.30
IRONFLOW_CANCEL_ON_REPLAY_BUFFER_SIZEMaximum entries retained in the cancel-on-replay buffer. Bounds memory under high event rates.10000
IRONFLOW_SQLITE_MAX_BYTESCaps 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_MAXCaps 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)
VariableDescriptionDefault
IRONFLOW_CAP_TOKEN_PRIVATE_KEY_B64Base64-encoded PKCS8 DER Ed25519 private key for minting deprovision capability tokens (ironflow cloud cap-token mint).(None, required for mint)
IRONFLOW_MASTER_KEY_B64Base64-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)
VariableDescriptionDefault
IRONFLOW_AUDIT_BATCH_SIZEMaximum rows per batch insert into the policy_decisions audit table.100
IRONFLOW_AUDIT_BATCH_FLUSH_MSMilliseconds between batch flushes. Set lower for tighter audit latency; set higher for write throughput.1000
IRONFLOW_AUDIT_KV_EMITSet 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_DAYSDays 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_FIELDSComma-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)
VariableDescriptionDefault
IRONFLOW_EVENT_SCHEMA_ENFORCEMENTWhether 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.

VariableDescriptionDefault
IRONFLOW_REBUILD_BATCH_SIZE_SQLITEEvents per batch when rebuilding a projection against a SQLite-backed event store.500
IRONFLOW_REBUILD_BATCH_SIZE_POSTGRESEvents per batch when rebuilding a projection against a PostgreSQL-backed event store.1000
IRONFLOW_REBUILD_BATCH_PAUSE_MSMilliseconds to pause between FULL rebuild batches (PG only). Useful for throttling large rebuilds.0

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.

VariableDescriptionDefault
IRONFLOW_PROJECTION_WAIT_CAP_PER_TENANTPer-organization ceiling on concurrent projection catch-up waits. Capped at the node-wide ceiling of 2000.500

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.


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.

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).


LimitValueApplies To
Max Event Data512 KBPayload of an emitted event.
Max Step Output1 MBData returned by a step.run.
Max Batch Size100Max events in a TriggerBatch call.
Wait Timeout30sDefault timeout for emit --wait.

IRONFLOW_EVENT_SCHEMA_ENFORCEMENT=reject treats two kinds of failure differently, because they have different owners.

What failedOwnerwarnreject
Payload does not match the registered schemaThe emitterLogs, accepts400, nothing written
Event name has no registered schemaNobody — the name is ungovernedAcceptsAccepts
Registered schema will not compileThe operator who registered itLogs, acceptsLogs, accepts
Registry read failed (store down, timeout, lock contention)InfrastructureLogs, accepts503, 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.