@samveys/flags

Versi
1.0.35 Nov 2026
Pasang
pnpm add @samveys/flags

Dari packages/flags/README.md di repository framework.

Halaman ini belum diterjemahkan. Kamu sedang membaca versi bahasa Inggris.

Feature flags with an OpenFeature-compatible evaluation API and a built-in database provider. No runtime dependencies.

Set up

ts
import { flags } from '@samveys/flags'

export const plugin = flags({ defaultContext: { environment: 'production' } })

The plugin binds 'flags' (a FlagClient). Without a provider it reads the table samveys_feature_flags(key, enabled, rollout_percent, variants JSON, rules JSON) from the 'db' binding and creates it on first use. A change in the table is visible to every process on the next evaluation.

Evaluate

ts
import { createDatabaseFlagProvider, createFlagClient } from '@samveys/flags'
import { createDatabaseManager, defineDatabaseConfig, sqlite } from '@samveys/database'

const db = createDatabaseManager(defineDatabaseConfig({ default: 'main', connections: { main: sqlite({ file: ':memory:' }) } }))
const provider = createDatabaseFlagProvider({ database: () => db })
const client = createFlagClient(provider)

export async function example(userId: string) {
  await provider.save({ key: 'new-checkout', enabled: true, rolloutPercent: 10 })
  const on = await client.getBooleanValue('new-checkout', false, { targetingKey: userId })
  const details = await client.getBooleanDetails('new-checkout', false, { targetingKey: userId })
  // details: { flagKey, value, variant: 'on' | 'off', reason: 'SPLIT' }
  return { on, reason: details.reason }
}

getBooleanValue, getStringValue, getNumberValue and getObjectValue return the value; the …Details versions return { flagKey, value, variant, reason, errorCode?, errorMessage? }. Evaluation never throws: an unknown flag, a missing targetingKey for a partial rollout, a wrong type or a failing database returns your default with reason: 'ERROR' and an errorCode (FLAG_NOT_FOUND, TARGETING_KEY_MISSING, TYPE_MISMATCH, GENERAL).

How a flag decides

  1. enabled = false → your default (reason: 'DISABLED').
  2. The first matching rule wins (reason: 'TARGETING_MATCH'): { attribute, operator, value, variant } with equals, notEquals, in, notIn, contains, startsWith, endsWith, gt, gte, lt, lte, tested against the evaluation context.
  3. rolloutPercent 100 → on; 0 → off.
  4. Otherwise the user's bucket decides (reason: 'SPLIT'): the first 4 bytes of SHA-256("<flagKey>:<targetingKey>") as an unsigned integer, modulo 100; the user is in when the bucket is below rolloutPercent.

The bucket depends only on the flag key and the user, so a user keeps their answer across requests, processes, machines and releases, and raising a rollout from 10 % to 25 % only adds users.

variants maps on / off (and any rule variant) to values: { "on": "dark", "off": "light" } for a string flag. Boolean flags need no variants.

ts
await provider.save({
  key: 'beta',
  enabled: true,
  rolloutPercent: 0,
  rules: [{ attribute: 'plan', operator: 'in', value: ['pro', 'team'], variant: 'on' }],
})