Groundfloor Docs

Feature flags API

Per-environment typed flags — list, get, upsert, delete, compare, promote, and the runtime read used by Shell apps and Coderunners.

Base path: /v1/workspaces/{workspace_id}/flags

See also: Feature flags (runtime examples) and the gf flags commands in Developers.

Environments

Every flag belongs to one environment. On list, get, upsert, delete and runtime, environment_id is an optional query parameter:

  • Omitted → the workspace default environment: the active environment named dev, or the only active environment if there is exactly one. The response echoes the resolved environment_id.
  • Passed → must be an environment of this workspace (404 otherwise) and active (409 otherwise).
  • No active environments at all → 409.

Pass it explicitly for anything that must not silently fall back to dev. List environment ids with GET /v1/workspaces/{id}/environments or gf environments ls.

Keys and values

Keys are trimmed and lowercased, then must match ^[a-z][a-z0-9_.-]{0,62}$: they start with a letter and continue with up to 62 lowercase letters, digits, -, _ or .. Examples: new-checkout, ui.dark_mode. Anything else is 400. compare, promote and runtime are reserved (400 on PUT).

Values are validated against value_type (default boolean). A mismatch is 422:

value_typeAcceptsStored as
booleantrue / false, or the strings "true" / "false"boolean
stringa string up to 10,000 chars; nullstring (null → "")
numberinteger or float (booleans rejected)number
jsonany JSON value, including nullas sent

description is optional, max 255 chars.

Auth

OperationPlatform token (gf token, portal)M2M tokenShell end-user JWTWorkload-identity JWT
GET /flags, GET /flags/{key}, GET /flags/compareworkspace read✅❌ 401 (use /runtime)❌ 403
GET /flags/runtimeworkspace read✅✅ with any role from the app's sign-in❌ 403
PUT /flags/{key}, POST /flags/promoteworkspace write❌ 403 (read-only)❌❌ 403
DELETE /flags/{key}workspace delete❌ 403 (read-only)❌❌ 403

read = owner, admin, writer, member. write / delete = owner, admin, writer. Writes are blocked with 423 while the workspace is frozen, and are audited (flag.upserted, flag.deleted, flag.promoted).

Runtime read

GET /v1/workspaces/{workspace_id}/flags/runtime?environment_id=

The app-facing read: a compact {key: value} map, for Shell federated apps and Coderunners.

{
  "workspace_id": "7965d229-0c0f-4e08-a096-21e04a0fb364",
  "environment_id": "0b1e…",
  "flags": { "new-checkout": true, "max-items": 25, "banner": { "text": "Hi" } }
}
  • Headers: weak ETag, Cache-Control: private, no-cache, Vary: Authorization.
  • Revalidate: send If-None-Match: <etag> → 304 Not Modified (no body) when nothing changed.
  • Polling: no more often than every 30 s. No push channel yet. There is no per-route rate limit, but be a good citizen.
  • Environment: Shell apps pass window.__GF_ENVIRONMENT_ID__; Coderunners pass GROUNDFLOOR_ENVIRONMENT_ID (injected on deploy).
  • Values are not secrets: every signed-in end user of the workspace can read them.

List / get / upsert / delete

MethodPathNotes
GET/v1/workspaces/{workspace_id}/flagsFull records (key, value, value_type, description, timestamps) ordered by key, plus the resolved environment_id
GET…/flags/{key}404 if not set in that environment
PUT…/flags/{key}Body: value, value_type, optional description. Creates or updates in one environment only.
DELETE…/flags/{key}204; 404 if not set in that environment
curl -s -X PUT -H "Authorization: Bearer $(gf token)" \
  -H "Content-Type: application/json" \
  -d '{"value":true,"value_type":"boolean","description":"New checkout flow"}' \
  "https://platform.groundfloor.cloud/v1/workspaces/$WORKSPACE_ID/flags/new-checkout?environment_id=$ENV_ID"

Compare and promote

GET  /v1/workspaces/{workspace_id}/flags/compare?source_environment_id=&target_environment_id=
POST /v1/workspaces/{workspace_id}/flags/promote

Compare returns one row per key with status only_source | only_target | both_same | both_differ, plus both values and types. Both ids are required.

Promote body:

{
  "source_environment_id": "…",
  "target_environment_id": "…",
  "keys": ["new-checkout"],
  "mode": "skip_existing",
  "dry_run": true
}

keys omitted → every source key. mode: skip_existing (default, leaves keys already in the target) or overwrite. The response lists copied, skipped and per-key failed (the call still returns 200).

For both, the environments must differ (400) and be active (409).

Errors

StatusWhen
400Invalid or reserved key; compare/promote with the same source and target
401Missing or invalid bearer token (including a Shell JWT on a management route)
403Missing workspace permission; M2M write; workload-identity token; site user without a site role on /runtime
404Unknown workspace, environment (in this workspace) or flag
409Environment not active; workspace has no active environments
422value doesn't match value_type; invalid body
423Workspace frozen (writes)

Errors use the standard {"detail": "…"} body. The OpenAPI document at /openapi.json carries the same descriptions per operation.