Appearance
Secrets vault
FlowRunner keeps your credentials — API keys, database URIs, bearer tokens — in a per-workspace vault, encrypted at rest with AES-256-GCM, and swaps them into a flow only at run time. This page covers the security model: how values are protected, how references resolve, and how to look after the encryption key.
What the vault protects
Each secret is a name and a value, scoped to one workspace and stored in the secrets collection keyed by { workspaceId, name }. Before a value touches the database it is encrypted with AES-256-GCM. The stored payload is iv:tag:ciphertext (all base64): a fresh random 12-byte IV per value, plus the GCM authentication tag that also detects tampering — a modified ciphertext fails to decrypt rather than returning garbage.
The encryption key is the SECRETS_KEY environment variable: a base64 string that must decode to exactly 32 bytes (256 bits). Anyone with only database access sees ciphertext; recovering plaintext requires that key, which lives in the server's environment, never in the database.
The vault is effectively write-only. Listing returns names only — a stored value is never sent back after it is set. If you forget a value, re-set it rather than trying to read it.
Resolving ${secrets.NAME} at run time
Anywhere a flow field takes a string you can write a reference instead of a literal. The saved flow only ever stores the placeholder, so exporting or sharing a project never exposes a credential. References resolve in node configuration (an HTTP Authorization header, a MongoDB connection URI, an email field) and in endpoint auth (a Start node's bearer token, basic password, or API key).
Decrypted values exist only in server memory for the duration of that one run, loaded freshly per execution.
Fail-closed: a typo cannot leak your environment
Resolution is fail-closed. If ${secrets.API_TOKEN} names a secret that does not exist, it resolves to an empty string — it does not fall back to a same-named server environment variable. A missing secret can break a request (fail-closed endpoint auth then rejects everyone, which is the safe outcome), but it can never silently substitute an unrelated env var. This closes the class of bug where a mistyped secret name quietly leaks server configuration into an outbound call.
Server environment variables have a separate, explicit escape hatch: ${env.NAME}, which reads process.env. Keep the two straight — ${secrets.*} is the encrypted vault, ${env.*} is server config. Only reach for ${env.*} when you deliberately want a process value.
WARNING
The vault protects secrets at rest. At run time, decrypted values are handed to node execution in memory. Today the engine runs only first-party built-in nodes, so decrypted secrets are only ever seen by trusted code. Server-side execution of untrusted third-party node logic is not sandboxed yet — do not treat the vault as isolation against custom code that does not exist on this platform today. See Node sandboxing.
Turning the vault on, and off
SECRETS_KEY is an optional variable. Generate one with openssl rand -base64 32. Without it the feature is simply disabled: setting a secret errors, the vault loads as an empty map, and every ${secrets.NAME} resolves to empty. The health check reports the vault as configured or disabled by presence only — it never leaks the key.
Rotating and backing up the key
WARNING
SECRETS_KEY is the only thing that can decrypt your secrets. Back it up separately from the database. A database backup alone is useless without the matching key, and losing the key strands every stored secret permanently.
Rotating the key does not re-encrypt existing values. After a rotation, old ciphertext no longer decrypts; the vault silently skips any value it cannot decrypt rather than erroring, so the safe procedure is to re-set each secret under the new key. Treat SECRETS_KEY like a database password: store it in your platform's secret manager, back it up, and rotate it deliberately.