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.
When to mark a config as secret
Section titled “When to mark a config as secret”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.
Create a secret
Section titled “Create a secret”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.
from bifrost import config
await config.set("stripe_api_key", "sk_live_xxxxx", is_secret=True)is_secret=True is the only difference from a plaintext config. Pass it on every set() call — it’s not inferred.
Read a secret
Section titled “Read a secret”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.
UI behavior for secrets
Section titled “UI behavior for secrets”- 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.
Organization scoping
Section titled “Organization scoping”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_aaaUpdating a secret
Section titled “Updating a secret”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.
Portable exports null secret values
Section titled “Portable exports null secret values”bifrost export --portable writes the config row with its key, description, and config_type: secret intact, but with value: null. After importing the bundle into a target environment, re-enter the value in Settings → Configuration for each org that needs it. See Exporting and Importing.
Best practices
Section titled “Best practices”- 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.
Troubleshooting
Section titled “Troubleshooting”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 redeploy — BIFROST_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.