Groundfloor Docs

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

MethodPathPermissionResponse
GET/v1/workspaces/{id}/secretsreadList keys (values never included)
GET/v1/workspaces/{id}/secrets/{key}readPlaintext value — audited on every call
PUT/v1/workspaces/{id}/secrets/{key}writeUpsert; returns metadata only
DELETE/v1/workspaces/{id}/secrets/{key}delete204 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

KeyPurpose
LITELLM_API_KEYWorkspace virtual key from LLM Gateway
OPENAI_API_KEYBYO provider — LiteLLM picks up for extra models
ANTHROPIC_API_KEYBYO provider
DATAPLANE_SERVICE_API_KEYPlatform-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

StatusMeaning
401Missing or invalid Keycloak token
403No read / write / delete on workspace
404Secret key not found (reveal/delete)
502Infisical / secret backend unreachable

Local dev: bring up Infisical per Phase 2 deps runbook and configure machine identity in .env.


On this page