Skip to content
Bifrost Docs

Storing Sensitive Config Values

Mark configs as secret to encrypt them at rest and mask them in the UI.

API keys, passwords, and webhook signing keys are stored as ordinary configs with config_type: secret. The platform encrypts the value at rest using Fernet, masks it in the UI, and excludes it from portable exports. There is no separate “secret” entity — secret is just a value type on Config, alongside string, int, bool, and json.

Mark it secret if leaking the value would compromise an account, allow impersonation, or violate compliance — API keys, OAuth client secrets, database passwords, webhook signing keys, third-party tokens. Don’t mark connection identifiers like tenant_id or company_id as secret; they’re not sensitive and masking them in the UI just makes debugging harder.

Settings → Configuration → + Add Config

  • Key: stripe_api_key
  • Type: Secret
  • Value: sk_live_xxxxx

The value is encrypted in the database before it’s persisted; you cannot read it back through the UI.

There’s no special read API. config.get() returns the decrypted value:

from bifrost import config
@workflow(name="charge_card")
async def charge_card(context: ExecutionContext, amount: int):
api_key = await config.get("stripe_api_key")
response = requests.post(
"https://api.stripe.com/v1/charges",
headers={"Authorization": f"Bearer {api_key}"},
json={"amount": amount},
)
return response.json()

The platform automatically registers the returned value with the execution-output scrubber, so it won’t appear in workflow logs or execution payloads even if your code accidentally returns it.

  • The value is masked (••••••) on the configuration list.
  • “Copy” and “Reveal” buttons are intentionally absent.
  • Editing requires re-entering the full value — there is no “show current value to edit” affordance.
  • Audit-log entries record reads and writes but never the value itself.

Secrets follow the same scope cascade as any other config (see Scopes). An org-specific secret shadows a global one with the same key:

# Org A's stripe_api_key = sk_live_aaa
# Org B's stripe_api_key = sk_live_bbb
# Workflow running in Org A's context:
api_key = await config.get("stripe_api_key") # sk_live_aaa

Set it again with the new value:

await config.set("stripe_api_key", "sk_live_new_value", is_secret=True)

In the UI, edit the config row and enter a new value. The previous value is overwritten — there is no version history.

A Solution can declare the config key and secret type, but portable source keeps its value empty. After installing the Solution in another environment, enter the value during setup or in Settings → Configuration for each org that needs it.

A full-backup Solution export can include selected config and secret values in a password-encrypted .bifrost/secrets.enc file. Use that only for an intentional backup or restore; normal source-controlled Solution packages remain secret-free. See Solutions.

  • Read the secret once near the top of your workflow and never log the variable.
  • Never include a secret in a return value or in form output.
  • Rotate the secret in the source system and update the Bifrost config in the same change window. Workflows pick up the new value on the next config.get() call — there’s no cache to invalidate.
  • For high-value secrets that need stronger custody (HSM-backed, audit pipelines, etc.), front Bifrost with an external secret manager and store only a short-lived token here.

KeyError on config.get("...") — the config doesn’t exist for this org. Check Settings → Configuration and confirm the scope matches your execution context.

Decryption errors after a redeployBIFROST_SECRET_KEY changed. The encryption key is derived from this env var; rotating it invalidates every existing encrypted value. Plan key rotations like a database migration.