Secrets
Read and write workspace secrets through the Control Plane secrets API.
Audience: App developers (server-side or trusted BFF)
Base path: /v1/workspaces/{workspace_id}/secrets
Auth: Keycloak bearer token + read / write / delete on workspace
Storage: Infisical (default) or customer BYO secret manager — transparent to your app
Control Plane owns which backend is bound to which workspace. Your app only talks to the Control Plane API.
Endpoints
| Method | Path | Permission | Response |
|---|---|---|---|
GET | /v1/workspaces/{id}/secrets | read | List keys (values never included) |
GET | /v1/workspaces/{id}/secrets/{key} | read | Plaintext value — audited on every call |
PUT | /v1/workspaces/{id}/secrets/{key} | write | Upsert; returns metadata only |
DELETE | /v1/workspaces/{id}/secrets/{key} | delete | 204 No Content |
List secrets
GET /v1/workspaces/{workspace_id}/secrets
Authorization: Bearer {keycloak_access_token}{
"secrets": [
{ "key": "LITELLM_API_KEY", "description": "Workspace LiteLLM virtual key" },
{ "key": "OPENAI_API_KEY", "description": "BYO OpenAI for LiteLLM routing" }
]
}Use this to populate a settings UI (key names and descriptions only).
Reveal a secret
GET /v1/workspaces/{workspace_id}/secrets/LITELLM_API_KEY
Authorization: Bearer {keycloak_access_token}{
"key": "LITELLM_API_KEY",
"value": "sk-…",
"description": "Workspace LiteLLM virtual key"
}Every reveal emits an audit event (secret.revealed). Do not call reveal from browser code that end users control. Prefer:
- Server-side route handler (Next.js Server Action / API route)
- Coderunner function with platform auth
- Dev-only: Portal UI or curl while building
Upsert a secret
PUT /v1/workspaces/{workspace_id}/secrets/MY_API_KEY
Authorization: Bearer {keycloak_access_token}
Content-Type: application/json
{
"value": "secret-value-here",
"description": "Optional human-readable note"
}Response (value not echoed):
{ "key": "MY_API_KEY", "description": "Optional human-readable note" }value max length: 10,000 characters.
Delete a secret
DELETE /v1/workspaces/{workspace_id}/secrets/MY_API_KEY
Authorization: Bearer {keycloak_access_token}Idempotent — deleting a missing key still succeeds with 204.
Common keys for federated apps
| Key | Purpose |
|---|---|
LITELLM_API_KEY | Workspace virtual key from LLM Gateway |
OPENAI_API_KEY | BYO provider — LiteLLM picks up for extra models |
ANTHROPIC_API_KEY | BYO provider |
DATAPLANE_SERVICE_API_KEY | Platform-managed — use POST /v1/workspaces/{id}/dataplane/provision (Portal: Authentication or Data Vault). Reveal only from server-side BFF / Coderunner, never in browser JS. |
Store app-specific integration keys here (Stripe, webhooks, etc.) instead of hardcoding in source or manifest.
TypeScript example (server route)
// app/api/internal/llm-key/route.ts — server only
export async function GET() {
const token = await getKeycloakTokenFromSession(); // your auth helper
const workspaceId = process.env.WORKSPACE_ID!;
const res = await fetch(
`${process.env.CONTROLPLANE_URL}/v1/workspaces/${workspaceId}/secrets/LITELLM_API_KEY`,
{ headers: { Authorization: `Bearer ${token}` } },
);
if (!res.ok) throw new Error(await res.text());
const { value } = await res.json();
return Response.json({ configured: Boolean(value) }); // never return value to browser in prod
}With @groundfloor/api-client, use the generated hooks/mutations from the secrets tag — still keep reveal server-side in production.
Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid Keycloak token |
403 | No read / write / delete on workspace |
404 | Secret key not found (reveal/delete) |
502 | Infisical / secret backend unreachable |
Local dev: bring up Infisical per Phase 2 deps runbook and configure machine identity in .env.
Related
- 07-llm-gateway.md — mint and store
LITELLM_API_KEY - 03-authentication.md — Keycloak token wiring
- 11-local-dev-recipes.md — curl examples