Skip to main content

Settings & feature flags

Orunbase gives every workspace a configuration store with two resource families: settings (typed key–value pairs) and feature flags (keyed on/off switches with an optional value). Both live at three scopes — workspace, project, and environment — and settings additionally support inherited resolution up a scope chain, so a value set once at the account or workspace level applies everywhere below unless a more specific scope overrides it.

Scopes

Each scope has its own collection path; the same operations work at every scope:

ScopeBase path
Workspace/v1/organizations/{orgId}/config
Project/v1/organizations/{orgId}/projects/{projectId}/config
Environment/v1/organizations/{orgId}/projects/{projectId}/environments/{envId}/config

Reads at workspace scope require organization.config.read; project and environment scopes require project.config.read. Writes require the corresponding …config.write action. A fourth rung — the account (the workspace's parent organization) — participates in resolved reads: values set at the account level are inherited by every workspace under it.

Manage settings

MethodPath (relative to a scope base)Description
GET/settingsList settings at exactly this scope
POST/settingsCreate a setting (key, value, optional description, valueType, visibility)
PATCH/settings/{settingId}Update a setting by its public id (stg_…), not its key (value, description, valueType, visibility)
GET/settings/resolve?key={key}Resolved read — walks the inheritance chain

There is no delete endpoint for settings or flags — the management surface is create, list, and update.

Typed values and visibility

A setting declares a valueTypestring, number, boolean, or json (the default) — and a visibility tier — public, internal (the default), or masked. On update, an omitted valueType or visibility keeps the stored declaration; supplying one re-declares it.

The server validates fail-closed, with two 422 validation_failed field-error shapes for the enums — valueType: ["Must be one of: string, number, boolean, json"] and visibility: ["Must be one of: public, internal, masked"] — and additionally validates the value against the declared type (a 422 on the value field when, say, a number setting is given "4").

Masked is not a secret

masked is a display tier: the value is hidden in lists and revealed on request in the console — but it is stored unencrypted, and reveals are not read-audited. If a value would hurt when leaked, it belongs in Secrets (write-only, AES-256-GCM at rest), not in a masked setting.

curl -X POST "https://api.orunbase.com/v1/organizations/org_1f2e3d4c/projects/prj_5e6f7a8b/config/settings" \
-H "Authorization: Bearer $ORUN_CLOUD_TOKEN" \
-H "Content-Type: application/json" \
-d '{"key": "deploy.max_concurrency", "value": 4, "description": "Cap concurrent deploys"}'
import { OrunCloud } from "@saas/sdk";

const client = new OrunCloud({
baseUrl: "https://api.orunbase.com",
auth: { kind: "bearer", token: process.env.ORUN_CLOUD_TOKEN! },
});

const scope = { kind: "project", orgId: "org_1f2e3d4c", projectId: "prj_5e6f7a8b" } as const;

const { setting } = await client.config.createSetting(scope, {
key: "deploy.max_concurrency",
value: 4,
});

await client.config.updateSetting(scope, setting.id, { value: 8 });

The SDK models the scope as a discriminated object ({ kind: "organization" | "project" | "environment", … }) so one method covers all three scopes.

Resolve a setting through the scope chain

GET …/config/settings/resolve?key=<key> returns the effective value at a scope by walking the resolution chain, most specific first:

environment → project → workspace → account → default

The first scope with a value wins. The response carries provenance — inheritedFrom.scopeKind tells you which rung supplied the value ("default" with no value means nothing is set anywhere in the chain):

curl "https://api.orunbase.com/v1/organizations/org_1f2e3d4c/projects/prj_5e6f7a8b/environments/env_3c4d5e6f/config/settings/resolve?key=deploy.max_concurrency" \
-H "Authorization: Bearer $ORUN_CLOUD_TOKEN"
{
"data": {
"setting": {
"id": "stg_4d5e6f7a8b9c",
"orgId": "org_1f2e3d4c5b6a",
"projectId": "prj_5e6f7a8b9c0d",
"environmentId": null,
"scopeKind": "project",
"key": "deploy.max_concurrency",
"value": 4,
"description": "Cap concurrent deploys",
"overridable": true,
"inheritedFrom": { "scopeKind": "project" },
"createdAt": "2026-06-12T09:30:00.000Z",
"updatedAt": "2026-06-12T09:30:00.000Z"
}
},
"meta": { "requestId": "req_7a8b9c0d1e2f", "cursor": null }
}

Contrast with GET …/settings, which is the management view: it lists only values defined at exactly that scope, with no inheritance.

Locked account guardrails

An account-scoped setting can be marked locked (overridable: false), making it a guardrail: any attempt to write the same key at a child workspace, project, or environment scope is rejected with 409 conflict. Because the override write is blocked, resolved reads never need to arbitrate — the account value is simply the only one that can exist. Settings created through the API default to overridable: true; use guardrails for org-wide policy values that individual workspaces must not loosen.

note

The account rung resolves fail-soft: if the parent account cannot be determined during a read, resolution falls back to environment → project → workspace → default rather than failing the request.

Feature flags

Feature flags live at the same three scopes with the same list/create/update surface under …/config/feature-flags. A flag has a flagKey, an enabled boolean, and an optional value payload for variants:

const { featureFlag } = await client.config.createFeatureFlag(
{ kind: "environment", orgId: "org_1f2e3d4c", projectId: "prj_5e6f7a8b", environmentId: "env_3c4d5e6f" },
{ flagKey: "checkout.new_flow", enabled: false, value: { rollout: 0 } },
);

// Flip it on
await client.config.updateFeatureFlag(
{ kind: "environment", orgId: "org_1f2e3d4c", projectId: "prj_5e6f7a8b", environmentId: "env_3c4d5e6f" },
featureFlag.id,
{ enabled: true, value: { rollout: 25 } },
);

Flags are updated by their public id (flg_…), not by key.

note

The …/resolve scope-chain read currently exists for settings only. Feature flags are scoped but read exactly-scope — resolve semantics for flags are a planned follow-up. (Don't confuse these tenant flags with Orunbase's plan flags like feature.multi_org, which come from billing entitlements.)

Settings vs. flags

Use a setting when…Use a feature flag when…
The value is configuration your code reads (limits, endpoints, tuning)The primary question is "is this behavior on here?"
You want inheritance — set once at workspace/account, override per environmentYou want an explicit per-scope switch with an optional variant payload
The value should be enforceable org-wide (overridable: false guardrails)You'll flip it independently per environment during a rollout

All setting, flag, and secret writes are recorded in the audit log under the config category.

In the console

Config and Feature flags are first-class rail entries — /orgs/{slug}/config and /orgs/{slug}/flags — sitting beside Secrets because the three share one scope lattice. Both pages currently manage the workspace rung only.

Config shows each key with its override/shadow chain: which rungs define the key, which one wins where, and what each shadowed rung would serve. Every row wears its visibility badge (public / internal / masked; masked values reveal on request).

Config edits redeploy consumers

Config resolves at plan time and enters the plan digest — so editing a value changes the digest of every plan that reads it, and the consumers redeploy on the next converge. A config edit is a deploy trigger, not a hot reload.

Feature flags shows each flag's rollout state — on, off, or partial — with the fall-through share spelled out explicitly (what fraction of the population falls through to each outcome). The page's evaluation previewer shares the worker's own bucketOf / pickVariant implementation, so the answer the console shows for a given subject is byte-for-byte the answer the worker serves. Changing a flag's rollout salt warns before applying: a new salt reshuffles everyone's bucket assignment, not just new subjects.

Variants, targeting rules, and rolloutSalt are console-visible but not yet creatable through the public API — the API surface for flags remains flagKey / enabled / value as documented above.