Secrets & Credentials
Workflows and agents need credentials — LLM API keys, database passwords, third-party API tokens, OAuth access tokens. OrcFlows stores all of these encrypted in PostgreSQL and resolves them into workflow expressions and connector calls at execution time. They are never written to logs or execution history in plaintext.
Encryption
- Both secrets (
internal/secrets/pgstore.go) and OAuth connections (internal/oauth/store.go) are encrypted with AES-256-GCM. - The encryption key is derived via SHA-256 from
JWT_SECRET— there is no separate key to manage, but it does mean rotatingJWT_SECRETinvalidates stored secrets/tokens (re-enter them after a rotation). - A previous design used HashiCorp Vault for secrets; this has been fully replaced by the Postgres-backed store. The
vaultcontainer may still appear indocker-compose.ymlbut is unused.
Manual secrets (API keys)
Simple named key/value pairs, scoped to your tenant:
| Endpoint | Description |
|---|---|
GET /api/v1/secrets | List secret names (values are never returned). |
POST /api/v1/secrets | Create/update a secret — {"name": "STRIPE_API_KEY", "value": "sk_..."}. Admin/Owner only. |
DELETE /api/v1/secrets/{name} | Remove a secret. Admin/Owner only. |
Reference a secret from any step config with {{ secret.NAME }}:
{ "type": "agent", "config": { "api_key": "{{ secret.ANTHROPIC_API_KEY }}" } }
At execution time, the workflow's ExecutionInput.Secrets map is populated by resolving every {{ secret.* }} reference used in the DSL — so only the secrets a workflow actually needs are decrypted for that run.
OAuth connections
For providers with OAuth flows (Google, Notion, Slack — see Configuration), OrcFlows stores a full Connection: access token, refresh token, expiry, scope, and the connected account's email/display name/avatar.
| Endpoint | Description |
|---|---|
GET /api/v1/oauth/status | Whether each provider (google, notion, slack) is configured at the platform level. |
GET /api/v1/oauth/{provider}/authorize | Starts the OAuth flow (public — browser redirect, takes a _token query param since redirects can't carry headers). |
GET /api/v1/oauth/{provider}/callback | OAuth redirect target (public). |
GET /api/v1/oauth/connections | List this tenant's connected accounts. |
DELETE /api/v1/oauth/connections/{id} | Disconnect. |
GET /api/v1/oauth/connections/{id}/token | Get a valid access token (auto-refreshes if expired and a refresh_token is present, e.g. Google). |
POST /api/v1/oauth/connections/{id}/refresh | Force a token refresh. |
Connector and agent-tool configs reference an OAuth connection by ID (resolved server-side to a fresh access token) rather than embedding tokens directly.
Manual connections
For connectors that use a static API key/token rather than OAuth (e.g. most of the connector reference), POST /api/v1/connections stores the credential alongside OAuth connections in the same encrypted table, so the credential picker in the UI shows one unified list regardless of auth type.
Credential picker UX
In the canvas, any node field that needs a credential (an API key, an OAuth connection) shows a picker populated from /api/v1/secrets and /api/v1/oauth/connections — you select a stored credential rather than pasting raw values into the workflow definition.
Best practices
- Use distinct secrets per environment (
dev/staging/prod— seeDefinition.Env) so a leaked staging key can't touch production data. - Prefer OAuth connections over long-lived API keys where the provider supports it (Google, Notion, Slack) — tokens can be revoked centrally and (for Google) auto-refresh.
- Rotate
JWT_SECRETonly with a plan to re-enter secrets/OAuth connections afterward, since it doubles as the encryption key.
Next
- Teams, Organizations & Roles — who can manage secrets.
- Connector Reference — per-connector auth requirements.