New:Socket for Asana Is Now Available.Learn more
Get Started

@ultimat3/flags

Package Overview
Dependencies
Maintainers
1
Versions
23
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ultimat3/flags

Feature flags: permanent switches, and temporary ones that cannot be forgotten

Source
npmnpm
Version
11.3.0
Version published
Weekly downloads
2.8K
177.09%
Maintainers
1
Weekly downloads
 
Created
Source

@ultimat3/flags

Feature flags: permanent switches, and temporary ones that cannot be forgotten.

Feature flags are normally a bad trade — N flags are 2^N states nothing tested. This package takes the trade only on terms that bound the exponent: every flag declares which of two kinds it is, and a temporary flag carries an expiry it cannot be declared without. Past that date, every evaluation reports the flag to the app's error monitor and the projection lists it as expired. The permanent set is a product surface; the temporary set is forced to shrink.

The two kinds

KindMeaningLifecycle rule
permanenta real product or ops switch — a plan capability, a kill switch, a rollout that became the productnone; it legitimately lives forever
temporaryscaffolding around an in-progress changeexpiresAt and owner are required; past the expiry every evaluation reports X_FLAG_EXPIRED

Omitting expiresAt on a temporary flag is a type error, not a lint rule — FlagExpiryIsMandatory in src/flag.ts is a compile-time assertion that fails tsc if the union is ever loosened. toFlag() re-checks it at runtime, because a store snapshot and a plain-JS caller have no types to be checked by.

Declaring

import { defineFlag } from '@ultimat3/flags';

export const dunning = defineFlag({
  kind: 'permanent',
  key: 'billing.dunning-emails',
  description: 'ops kill switch for dunning email delivery',
  targeting: { default: true },
});

export const newTaxEngine = defineFlag({
  kind: 'temporary',
  key: 'checkout.new-tax-engine',
  description: 'routes checkout through the rewritten tax engine',
  owner: 'payments',
  expiresAt: '2026-12-01',
  targeting: { default: false, rollout: 10, roles: ['staff'] },
});

defineFlag() is a define* helper, like defineRoles and defineCatalogs. It is not a ninth primitive: a flag has no handler, no input schema and no surface of its own, so there is nothing for the registrar to project. The eight primitives stay eight.

Reading

import { isEnabled } from '@ultimat3/flags';

if (isEnabled('checkout.new-tax-engine', actor)) {
  // …
}

// with the app's own records in play
if (isEnabled('scraper.persist-profile', actor, { bank: bank.id })) {
  // …
}

Synchronous, for the same reason can() is: this runs inside policy predicates and render passes, and an await there turns every guarded branch into an async boundary. An undeclared key throws X_FLAG_UNKNOWN rather than answering false — a typo that reads as "off" is a branch that silently never runs in production.

Targeting

FieldMeaning
defaultthe answer when no allow list and no rollout claims this actor
actorsactor ids that are always on, ahead of any rollout
rolesactor roles that are always on, ahead of any rollout
orgsorg ids that are always on — shorthand for the org subject kind, read from actor.orgId
subjectsallow lists for the app's own record kinds: { bank: ['bank_integration:bbva'] }
rolloutwhole percentage 0-100, stable per bucketing subject
bucketBywhich subject kind the rollout divides: 'actor' (default), 'org', or any kind the call site carries

Order is allow lists → rollout → default. actors, roles, orgs and subjects are one rank — any hit is true. An operator who names a subject is not overruled by a hash. A rollout buckets fnv1a(key + ':' + subjectId) % 100, never Math.random(): one subject gets one answer on every call, in every process, without the nodes talking to each other.

Subjects — what a flag decides about

A flag decides about an identified record: a user, a tenant, a bank integration, a device. The actor is one subject kind among several, not a privileged one.

KindWhere its id comes from
actoractor.id — spelled actors in targeting
orgactor.orgId — spelled orgs in targeting
anything elsethe subjects argument at the call site
// whole tenants, named — the 90% case, which is why it has a shorthand
targeting: { default: false, orgs: ['org_acme'] }

// 10% of tenants, each one whole
targeting: { default: false, rollout: 10, bucketBy: 'org' }

// the app's own record kind
targeting: { default: false, subjects: { bank: ['bank_integration:bbva'] } }
isEnabled('scraper.persist-profile', actor, { bank: 'bank_integration:bbva' });

// 10% of banks, each bank whole
targeting: { default: false, rollout: 10, bucketBy: 'bank' }

actor and org are resolved from the Actor and never from the call-site map — one source per kind, so there is no precedence rule to remember and no second place a tenant can come from. Every other kind is the app's vocabulary; the kind space is open, exactly like the flag key space.

Bucketing by a record is what keeps it whole. An actor-bucketed rollout cuts through a tenant: 3 of an org's 30 members on the new export path and 27 on the old, sharing documents, filing a bug nobody can reproduce. bucketBy puts the whole subject on one side. 'actor' stays the default, so every flag declared before this axis answers exactly as it did.

roles is deliberately not a subject kind: a role is a predicate over the actor, not an identified record, so it has no id to hash and cannot bucket a rollout.

A missing subject is an error, not a fallback

If targeting decides by a kind the evaluation context does not carry, isEnabled() throws X_FLAG_SUBJECT_REQUIRED. It never falls back to the actor axis or to default: an answer about a record computed from whoever happened to be calling is the exact failure this axis removes, and it looks like it worked. Every declared kind is resolved before any of them can answer, so the raise does not depend on the order the keys sit in.

A null actor is the one exception and still gets default — it says there is no evaluation context at all, every such call answers alike, and no single subject is split.

Overrides, out of band

applyFlagSnapshot({ 'billing.dunning-emails': { default: false } });

This is the half that keeps evaluation synchronous. A poller, a job or a realtime channel lands the store's targeting; isEnabled() never loads anything. Keys this build does not declare are returned in unknown rather than thrown — a control plane is routinely ahead of a deploy, and a kill switch that refuses to land because the payload mentioned tomorrow's flag is one that does not work on the day it is needed.

Reporting

There is no reporter seam in this package. An overdue flag goes through @ultimat3/core's ErrorReporter — the framework's one error-monitoring seam — as a warning from source: 'process', so an app wires its monitor in exactly one place:

configureErrorReporting({ reporter: sentryErrorReporter({ dsn }) });

What this package adds is the rate limit core has no opinion about: one report per flag per DEFAULT_REPORT_INTERVAL_MS (1 hour), on the monotonic clock, so a flag read on every request does not become the loudest thing in the monitor — which is how a report that fires per call ends up muted and the debt invisible again. configureFlags({ clock, reportEveryMs }) tunes it.

Projection

flagsReport() returns every declared flag with its kind, expiry, owner and whether it is expired, sorted by key, plus the expired keys lifted out. It is the one shape x flags --json, an MCP tool and the manifest should all read, so none of them recomputes "expired".

What it owns

ModuleOwns
src/flag.tsthe two kinds, the compile-time expiry rule, normalisation
src/targeting.tswho a flag is on for; declaration-time validation
src/subject.tswhat a flag decides about — subject kinds and how each resolves to an id
src/bucket.tsthe stable (flag, subject) bucket — FNV-1a, never Math.random()
src/registry.tsdefineFlag, key → flag, applyFlagSnapshot
src/runtime.tsthe clock and the per-flag report rate limit over core's reportError
src/evaluate.tsisEnabled() — the one way to ask
src/projection.tsflagsReport() for the CLI, MCP and the manifest
src/errors.tsthis package's X_* codes

Boundary

Tier 1. May import tiers 0-0 only — enforced by bun run scripts/boundaries.ts.

Errors

X_FLAG_DUPLICATE · X_FLAG_EXPIRED · X_FLAG_EXPIRY_INVALID · X_FLAG_SUBJECT_REQUIRED · X_FLAG_TARGETING_INVALID · X_FLAG_UNKNOWN

FAQs

Package last updated on 24 Aug 2026

Related posts