features
Feature flags turn product features on and off per tenant without a deploy.
Feature flags turn product features on and off per tenant without a deploy. Flags the root tenant defines are
platform flags and reach every organization and project; flags an organization or project defines reach only its own
subtree. A key belongs to the tenant closest to the root, so a tenant can never shadow a platform flag. Every flag is
a boolean, read by applications through evaluate, by server code through iam.features (evaluate, values,
isEnabled, no credential), and by policies through the tenant.features condition key. The repository guide is
docs/feature-flags.md.
How a value is decided
For a given tenant, the first rule that applies decides:
- The kill switch (
killSwitch: true): off everywhere. - The closest target or override on the way from the tenant up to (not including) the defining tenant. A target
is a value the flag's managers pin for a tenant below them (
setTarget); an override is a tenant's own choice for atenantOverridableflag (setOverride). On the same tenant the override wins; alockedtarget silences overrides at its tenant and below. Targets can lapse (expiresAt). - The rollout (
rolloutPercentage, 0 to 100): a stable share of the branches directly below the defining tenant getstrue, so an organization and its projects land on the same side. Raising it only adds tenants. - The default (
defaultValue).
Resources are iam/features/{key} (and iam/features for list), so a policy can delegate one flag or a family,
such as iam:features:override on iam/features/beta-*.
| Method | What it does | Access |
|---|---|---|
create | Defines a flag in the tenant: a platform flag on the root tenant, a flag for the tenant's subtree elsewhere. | Credential |
delete | Deletes a flag together with every target and override set for it. | Credential |
evaluate | Returns { tenantId, flags: { key: boolean } } for the flags that reach the tenant, for applications deciding what to show. | Credential |
list | The flags that reach the tenant, sorted by key, each with its value for the tenant and why. | Credential |
listTargets | Every target and override below the defining tenant for one flag, newest first. | Credential |
setOverride | Records a tenant's own choice for a flag an ancestor defines with tenantOverridable: true. | Credential |
setTarget | Pins a flag's value for a tenant below the defining tenant (tenantId), and for that tenant's descendants. | Credential |
update | Changes a flag's settings. Fields left out keep their value. | Credential |
create
Defines a flag in the tenant: a platform flag on the root tenant, a flag for the tenant's subtree elsewhere.
Used inFeature flags
- Permission:
iam:features:manageoniam/features/{key}. - Audited as:
feature:create, with the settings. - Errors:
CONFLICT(409) when the tenant or one of its ancestors already defines the key;LIMIT_EXCEEDED(409) past 200 flags in the tenant;INVALID_INPUTfor a malformed key, an unknown field,rolloutPercentagewithdefaultValue: true, or aninternalflag that is alsotenantOverridable;INVALID_TRANSITIONin a deleted tenant.
Keys are lowercase letters and digits joined by -, _, or ., starting with a letter, at most 64 characters.
Settings default to off, not overridable, no kill switch, not internal. internal flags are evaluated only by server
code and policies and are hidden from tenants below the defining one.
await iam.api.features.create(rootCredential, {
tenantId: rootTenantId,
key: 'new-billing',
description: 'The redesigned billing pages',
tenantOverridable: true,
});Prop
Type
A FeatureFlagDefinition object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/features/create" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"key": "<key>"
}'iam.api.features.create(
credential: CredentialInput,
input: {
tenantId: string;
key: string;
description?: string;
defaultValue?: boolean;
rolloutPercentage?: number;
tenantOverridable?: boolean;
killSwitch?: boolean;
internal?: boolean;
},
): Promise<FeatureFlagDefinition>delete
Deletes a flag together with every target and override set for it.
- Permission:
iam:features:manageoniam/features/{key}. - Audited as:
feature:delete, withremovedTargets. - Errors:
NOT_FOUNDwhen the tenant does not define the key.
Code that still asks for the key gets false (reason UNKNOWN). The result is { success: true, removedTargets }.
Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/features/delete" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"key": "<key>"
}'iam.api.features.delete(
credential: CredentialInput,
input: { tenantId: string; key: string },
): Promise<{ success: true; removedTargets: number }>evaluate
Returns { tenantId, flags: { key: boolean } } for the flags that reach the tenant, for applications deciding what to show.
Used inFeature flags
- Permission: None beyond a session (user, API key, role, or session token) of the tenant; root administrators may evaluate any tenant.
- Audited as: not audited.
- Errors:
ACCESS_DENIED(403) for a session of another tenant;INVALID_INPUTfor a malformed key or more than 100 keys.
Internal flags of ancestors are left out. keys limits the answer to those flags, and a requested key that no flag
defines comes back false. The React hooks useFeatureFlags and useFeatureFlag call it.
const { flags } = await client.features.evaluate({ tenantId });
if (flags['new-billing']) showNewBilling();Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/features/evaluate" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.features.evaluate(
credential: CredentialInput,
input: { tenantId: string; keys?: string[] },
): Promise<{ tenantId: string; flags: Record<string, boolean> }>list
The flags that reach the tenant, sorted by key, each with its value for the tenant and why.
- Permission:
iam:features:readoniam/features. - Audited as:
iam:features:read.
Each entry has evaluation (value, reason of KILL_SWITCH, TARGET, OVERRIDE, ROLLOUT, or DEFAULT,
decidedBy, expiresAt, locked, and overridable), the tenant's own override, and the target pinned for it.
Flags the tenant defines also carry definition with their settings. Ancestors' internal flags are left out, except
for root administrators, who see every flag with its definition. shadowed lists flags this tenant defines whose key
an ancestor also defines, so the ancestor's flag applies. Rename or delete them.
Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/features/list" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.features.list(
credential: CredentialInput,
input: { tenantId: string },
): Promise<{
tenantId: string;
flags: FeatureFlagView[];
shadowed: FeatureFlagDefinition[];
}>listTargets
Every target and override below the defining tenant for one flag, newest first.
- Permission:
iam:features:readoniam/features/{key}, in the defining tenant. - Audited as:
iam:features:read. - Errors:
NOT_FOUNDwhen the tenant does not define the key.
Each entry names the tenant (tenantName, tenantStatus), the source (target or override), the value,
locked, expiresAt, the note, and active: false once a target has lapsed or an override is no longer allowed.
Only the defining tenant's managers see notes.
Prop
Type
An array of FeatureTargetView.
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/features/listTargets" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"key": "<key>"
}'iam.api.features.listTargets(
credential: CredentialInput,
input: { tenantId: string; key: string },
): Promise<FeatureTargetView[]>setOverride
Records a tenant's own choice for a flag an ancestor defines with tenantOverridable: true.
Used inFeature flags
- Permission:
iam:features:overrideoniam/features/{key}, in the tenant. - Audited as:
feature:override, with the value (nullwhen withdrawn). - Errors:
FEATURE_LOCKED(409) when the flag does not allow overrides or a locked target covers the tenant;NOT_FOUNDfor a key no flag defines or an ancestor's internal flag;INVALID_INPUTwhen the tenant defines the flag itself (change it withupdate).
The choice applies to the tenant and its descendants unless something closer decides. value: null withdraws it,
which is always allowed. The result is the tenant's new evaluation.
Prop
Type
A FeatureEvaluation object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/features/setOverride" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"key": "<key>",
"value": false
}'iam.api.features.setOverride(
credential: CredentialInput,
input: { tenantId: string; key: string; value: boolean | null },
): Promise<FeatureEvaluation>setTarget
Pins a flag's value for a tenant below the defining tenant (tenantId), and for that tenant's descendants.
Used inFeature flags
- Permission:
iam:features:manageoniam/features/{key}, in the defining tenant. - Audited as:
feature:target, recorded in the defining tenant withtargetTenantId,value,locked, andexpiresAt. - Errors:
INVALID_INPUTwhentargetTenantIdis not below the defining tenant (usedefaultValuefor the tenant itself) orexpiresAtis not in the next ten years;NOT_FOUNDwhen the tenant does not define the key.
locked: true keeps the tenant and everything below it from overriding. expiresAt makes the target lapse by
itself, for a trial or a temporary block. note is visible only to the defining tenant's managers. value: null
removes the target.
await iam.api.features.setTarget(rootCredential, {
tenantId: rootTenantId,
key: 'fast-search',
targetTenantId: acmeId,
value: true,
expiresAt: Date.now() + 14 * 86_400_000,
note: 'Design partner trial',
});Prop
Type
One of null | FeatureTargetView; the object form has these fields:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/features/setTarget" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"key": "<key>",
"targetTenantId": "<targetTenantId>",
"value": false
}'iam.api.features.setTarget(
credential: CredentialInput,
input: {
tenantId: string;
key: string;
targetTenantId: string;
value: boolean | null;
locked?: boolean;
expiresAt?: number;
note?: string;
},
): Promise<FeatureTargetView | null>update
Changes a flag's settings. Fields left out keep their value.
Used inFeature flags
- Permission:
iam:features:manageoniam/features/{key}. - Audited as:
feature:update, with the settingsbeforeandafter. - Errors:
NOT_FOUNDwhen the tenant does not define the key;INVALID_INPUTas forcreate.
null clears description or rolloutPercentage. Turning killSwitch on switches the flag off for every tenant at
once, and turning it off restores the stored targets, overrides, and rollout. Turning tenantOverridable off keeps
existing overrides but ignores them.
Prop
Type
A FeatureFlagDefinition object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/features/update" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"key": "<key>"
}'iam.api.features.update(
credential: CredentialInput,
input: {
tenantId: string;
key: string;
description?: string | null;
defaultValue?: boolean;
rolloutPercentage?: number | null;
tenantOverridable?: boolean;
killSwitch?: boolean;
internal?: boolean;
},
): Promise<FeatureFlagDefinition>Better IAM is created by Sean Filimon
Last updated