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:
| Scope | Base 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
| Method | Path (relative to a scope base) | Description |
|---|---|---|
GET | /settings | List settings at exactly this scope |
POST | /settings | Create 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 valueType — string, 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 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.
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.
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 environment | You 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 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.