Skip to content

Feature flags

Built-in feature flags with targeting rules, percentage rollout, multivariate variations, and an OpenFeature-aligned eval API.

Vaultbase ships a real feature flag system — not a glorified settings table. Boolean / string / number / JSON flags, ordered targeting rules, percentage rollouts with sticky bucketing, and an admin UI at /_/flags.

CREATE TABLE vaultbase_feature_flags (
key TEXT PRIMARY KEY,
description TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT 'bool', -- bool | string | number | json
enabled INTEGER NOT NULL DEFAULT 1, -- master kill switch
default_value TEXT NOT NULL DEFAULT 'false',-- JSON-encoded scalar
variations TEXT NOT NULL DEFAULT '[]', -- JSON: named variations
rules TEXT NOT NULL DEFAULT '[]', -- JSON: ordered targeting rules
created_at, updated_at INTEGER
);

A flag’s effective value is computed by walking rules top-to-bottom and returning the first matching rule’s variation. No match → default_value. enabled = 0 short-circuits to default — the kill switch.

{
"id": "9b4c1a2",
"when": {
"all": [
{ "attr": "user.plan", "op": "in", "value": ["pro", "enterprise"] },
{ "attr": "user.email", "op": "ends_with", "value": "@acme.com" }
]
},
"rollout": { "value": 25, "sticky": "user.id" },
"variation": "treatment_a"
}

when is a boolean tree (all / any / not / segment / leaf). { "segment": "<name>" } matches a saved segment’s conditions (segments are managed under /api/v1/admin/flag-segments, or with Segments on the admin’s flags page). The admin edits a rule’s conditions visually when they’re a single all list of checks, and as JSON (with its prerequisites) otherwise. Operators:

OpNotes
eq neqStrict equality.
in not_invalue must be an array.
contains starts_with ends_withString only.
gt gte lt lteNumber only.
betweenvalue: [min, max] inclusive.
existsAttribute is present and not null (ignores value); false, 0 and "" count as present.
regexvalue is a regex string. Anchors not implicit.

Attributes are dotted paths into the evaluation context (user.plan, request.country, feature_flags.beta_optin).

rollout.value is 0-100. The hash of flag_key + ":" + context[rollout.sticky] mod 100 must be < the rollout value for the rule to fire. Deterministic — same user lands in the same bucket across processes — and sticky — once you’re in, you stay in even if other rules change.

A losing roll causes the engine to fall through to the next rule, so a follow-up rule with no rollout can still pick up the long tail.

A rule can require other flags first:

{ "id": "p1", "prerequisites": [{ "flag": "checkout_variant", "variation": "treatment_a" }], "variation": "true" }

The rule fires only when every listed flag evaluates to that variation (or value) for the same context; otherwise the engine moves to the next rule. Prerequisites nest up to 8 levels.

Server-side evaluation (hooks / routes / cron)

Section titled “Server-side evaluation (hooks / routes / cron)”
// inside a hook
const enabled = await ctx.helpers.flags.isEnabled("new_checkout", {
auth: ctx.auth,
});
const variant = await ctx.helpers.flags.getString("checkout_variant", "control", {
auth: ctx.auth,
});
const cfg = await ctx.helpers.flags.getJson<{ brand: string }>("payment_cfg", { brand: "stripe" }, {
auth: ctx.auth,
});

The helpers use the context exactly as you pass it. ctx.auth is the verified caller; passed as auth, flag rules can target auth.id, auth.type, auth.email, auth.collection, etc., the same attributes the public eval API sets from the session.

For client SDKs and external services:

POST /api/v1/flags/evaluate
{
"context": { "user": { "id": "u1", "plan": "pro" } },
"keys": ["new_checkout", "checkout_variant"] // optional — omit for all
}
→ { "data": { "new_checkout": true, "checkout_variant": "treatment_a" } }

No auth required by default (flags are presentation-only — the targeting context the caller sends is what determines their treatment). Don’t put secrets in flag values.

The auth attribute is the exception: the server sets it from the caller’s session — auth.id, auth.type, auth.email, auth.collection, auth.anonymous — and drops any auth the caller sent, so rules that must not be spoofed should target auth.*. Set flags.public_evaluate to 0 to answer 401 to callers without a session.

GET /api/v1/admin/flags — list
GET /api/v1/admin/flags/:key — read one
POST /api/v1/admin/flags — create
PATCH /api/v1/admin/flags/:key — update (partial)
DELETE /api/v1/admin/flags/:key — delete
POST /api/v1/admin/flags/:key/evaluate — test-context preview

All require admin auth and emit audit-log rows under flags.* actions.

/_/flags shows a two-pane editor:

  • Left — list of flags. Color dot for enabled state, type + rule count in the meta line.
  • Right — full editor: kill switch, key + type, description, default value, variations (multivariate flags), targeting rules with conditions
    • rollout sliders.
  • Test context panel — paste JSON, click Evaluate, see the trace: resolved value, variation, reason (rule_match, disabled, no_match, missing), and which rule id matched.

The eval surface mirrors the OpenFeature provider spec:

  • Variations are named buckets, not anonymous values.
  • Evaluation context is a freeform object, not a fixed user shape.
  • evaluate() returns value + reason + variation, not just the value (so callers can log telemetry on flag decisions).

The SDK ships an OpenFeature provider, VaultbaseFlagsProvider from @vaultbase/sdk/openfeature (see SDK feature flags).

  • Single-process cache — flags refresh from DB every 5 s. A fleet of vaultbase instances all converge within that window. Admin changes are also pushed on the __flags realtime topic (flag_changed / flag_deleted), which the SDK uses to re-evaluate.
  • No experimentation — flag values are reported, but conversion metrics aren’t bound to them. Phase 3.
  • Targeting context is caller-supplied — vaultbase doesn’t infer it, except auth.* on the public eval API, which comes from the session. In hooks, ctx.auth is canonical.