Secrets & Vaults
First-class, feature-agnostic project secrets — stored encrypted or fetched live from a connected HashiCorp Vault.
A secret is a reusable, project-scoped credential that is decoupled from the feature that consumes it. You create and manage secrets once, then reference them from features — today the only consumer is project-scoped Bring-Your-Own-LLM (custom LLM backends), but a secret can back any future feature without re-entering the value.
Secrets live on the project's Secrets tab in the dashboard
(/projects/{projectId}/secrets). The whole page is admin-only, consistent
with Custom LLMs.
Sources
Each secret resolves to a plaintext value from one of two sources:
| Source | Where the value lives | When to use |
|---|---|---|
manual | Encrypted (AES-256-GCM) and stored in the control plane | A one-off key you paste in; the platform holds it |
vault | Fetched live from the project's connected vault (vaultPath + vaultField) | Your secrets already live in HashiCorp Vault |
A secret's value is never returned in list views — only a masked hint for
manual secrets. An explicit Reveal action returns the plaintext (decrypting
locally for manual, or fetching from the vault for vault).
Decoupled by design
A secret is just a named credential. Features reference it by id (or name), not by the value itself. Rotate or replace the underlying value and every feature that points at the secret picks up the change automatically.
Vaults
A project may connect at most one external secret store (a vault). Today
only HashiCorp Vault is supported. Vault-backed secrets (source: vault)
are resolved against this connection at request time; if no vault is connected,
creating vault secrets is disabled.
Authentication methods
The platform authenticates to your vault using the common HashiCorp Vault auth methods. Credentials are stored encrypted as a JSON blob and decrypted only transiently to log in.
| Method | Credentials | Login |
|---|---|---|
token | a static token | none — the token is used directly (verified via lookup-self) |
approle | roleId, secretId, optional mount | POST /v1/auth/{mount}/login with {role_id, secret_id} |
userpass | username, password, optional mount | POST /v1/auth/{mount}/login/{username} with {password} |
Auth mounts
Each method defaults to its conventional mount (approle, userpass).
Override it with the optional auth mount field if your Vault uses a
non-default path (e.g. a dedicated mount per team).
Reading secrets
Secrets are read from a KV secrets engine. Configure the mount path
(default secret) and KV version:
- KV v2 — data lives under
data/, so a pathmyapp/prodis read from/v1/{mount}/data/myapp/prod. - KV v1 — read directly from
/v1/{mount}/{path}.
Each vault secret can name a field (e.g. api_key) to extract from the
secret object. If omitted, the platform picks a conventional key
(value / api_key / token / secret) or the first string value.
Caching & connection health
- Login tokens are cached in-process per vault configuration (≤ 5 min) so the platform doesn't authenticate on every read.
- Resolved values are cached briefly (stale 30s / expire 60s) to avoid hammering your vault on every chat request. Editing or deleting a secret invalidates its cached value immediately.
- Use Test connection on the Secrets tab to verify the vault. The status
(
Connected/Error/Unverified) and last error are shown inline.
Bring-your-own LLM (custom backends)
Custom LLM backends route chat requests for a project to your own endpoint
with your own credentials. Agents see them in the model catalog prefixed
with custom: (e.g. custom:acme-gpt), and requests bypass the project
request limit.
A backend's credential comes from either a referenced secret or a raw API key entered directly:
- Secret (preferred) — pick a project secret from the dropdown. The backend
stores a reference (
secretId) and never holds the value itself. - Raw API key (legacy) — paste a key; it is encrypted at rest. Useful when you don't want to create a standalone secret.
At request time the resolver prefers the linked secret and falls back to the raw key, so existing backends keep working unchanged.
Single-target routing
Custom backends are deliberately excluded from the global round-robin, metrics, and availability pool. Each is a single target that retries on network errors in isolation — there is no cross-backend failover.
Security
Secrets are treated as compromised-by-default:
- Encryption at rest:
manualvalues and vault auth credentials are encrypted with AES-256-GCM (random salt + IV per record, key derived fromBETTER_AUTH_SECRETvia scrypt). - No plaintext in list views: only masked hints are returned; the full value requires an explicit, admin-gated Reveal.
- Transient decryption: plaintext exists only long enough to forward the credential to the LLM backend or to log in to the vault — it is never written to logs or traces.
- Project scoping: secrets belong to one project; deleting a project cascades to its secrets, vault, and custom backends.
API
The Custom LLM endpoints accept a secretId alongside the legacy apiKey.
Provide one or the other:
// POST /api/projects/{projectId}/custom-llms
{
"profileName": "acme-gpt",
"model": "gpt-4o",
"apiBase": "https://api.example.com/v1",
"secretId": "<project secret id>"
}
Selecting a secret clears any raw key, and vice-versa. See Projects API for the full endpoint reference and Security for encryption details.