JavaScript SDK

The @flagify/node package is the core SDK for evaluating feature flags in any JavaScript or TypeScript runtime.

Installation

npm install @flagify/node

Initialization

import { Flagify } from '@flagify/node';

const flagify = new Flagify({
  projectKey: 'my-project',
  publicKey: 'pk_dev_abc123_xxxxxxxx',
  options: {
    realtime: true,       // enable SSE streaming for live updates
    staleTimeMs: 300000,  // cache stale time (5 min)
  },
});

Configuration options

OptionTypeDefaultDescription
projectKeystringYour project key (required)
publicKeystringPublishable API key (required)
secretKeystringSecret API key (server-side only)
options.realtimebooleanfalseEnable real-time flag updates via SSE
options.staleTimeMsnumberCache stale threshold in ms
options.apiUrlstringhttps://api.flagify.devCustom API base URL
options.pollIntervalMsnumberPolling interval in ms for periodic flag sync (fallback for environments without SSE)
options.sseIdleTimeoutMsnumber45000Silence watchdog for the realtime stream. If no bytes arrive in this window the client aborts and reconnects. Catches zombie TCP connections. Should be at least 2–3× the server heartbeat interval (15s) so a single missed heartbeat under jitter does not trip a reconnect. The watchdog polls every 10s, so values below ~20s are not useful.
options.sseReconnectBaseMsnumber1000Base delay for the SSE reconnection exponential backoff. Actual delay is jittered to 50–100% of the exponential value and honors the server’s retry: field as a floor.
options.sseReconnectMaxMsnumber30000Cap for the SSE reconnection exponential backoff.
options.userFlagifyUserUser context for targeting

Evaluating flags

Boolean flags

const isEnabled = flagify.isEnabled('feature-name');

isEnabled() returns the flag’s evaluated value (not the enabled state). When the flag is disabled, it returns the flag’s offValue. When the flag doesn’t exist in cache, it returns false.

Getting a flag value

const maxRetries = flagify.getValue<number>('max-retries', 3);

When the flag is enabled, getValue() returns the flag’s current value. When disabled, it returns the flag’s offValue from the server. The fallback parameter is only used when the flag doesn’t exist in cache.

Multivariate flags (A/B testing)

Use getVariant() to get the winning variant key based on weight:

const variant = flagify.getVariant('checkout-flow', 'control');
// Returns the variant key with the highest weight, e.g. 'variant-a'

Real-time updates (SSE)

When realtime: true is set, the SDK opens a Server-Sent Events connection to the Flagify API. When a flag is changed (toggled, updated, promoted), the SDK automatically refetches that specific flag and updates the cache.

const flagify = new Flagify({
  projectKey: 'my-project',
  publicKey: 'pk_dev_abc123_xxxxxxxx',
  options: {
    realtime: true,
  },
});

// Flags are updated automatically — no polling needed
const enabled = flagify.isEnabled('new-checkout');

The SSE connection:

  • Reconnects automatically with exponential backoff (1s to 30s, jittered, honors the server’s retry: field as a floor)
  • Receives heartbeats every 15s from the server to keep the connection alive (the client watchdog defaults to 45s of silence before forcing a reconnect)
  • Only refetches the specific flag that changed (not all flags)

Debug logging

The SDK is silent in normal operation. To diagnose realtime/SSE issues, opt in with the FLAGIFY_DEBUG env var:

FLAGIFY_DEBUG=1 node app.js

You’ll see entries like [Flagify] Realtime connected, [Flagify] Synced N flags via SSE, [Flagify] Flag changed: <key>, and [Flagify] SSE idle for >45000ms — forcing reconnect. Errors that indicate non-recoverable problems (failed evaluation after sync, duplicate connect() calls, auth failures, SSE parse errors) always log regardless.

In a browser without bundler env-var inlining, use:

localStorage.setItem("FLAGIFY_DEBUG", "1");
location.reload();

The flag is read once at module load, so the page must reload after toggling it.

Polling vs SSE vs stale-while-revalidate

The SDK has three ways to keep the cache fresh. Pick the one that matches your runtime.

StrategyOptionWhen to useTrade-off
SSE streamingrealtime: trueLong-lived servers, browsers, anywhere you can hold a connectionInstant updates. Requires a persistent HTTP connection.
PollingpollIntervalMs: 60000Edge/serverless runtimes that block SSE, or strict corporate proxiesPredictable network pattern. Up to one poll interval of staleness.
Stale-while-revalidatestaleTimeMs: 300000Any runtime — complements the other twoisEnabled() / getValue() returns the cached value immediately; if it’s older than staleTimeMs, a background refetch fires. Reads never block.

The three are independent and composable: realtime: true for push, staleTimeMs as a safety net against missed events, and pollIntervalMs as a fallback when SSE isn’t an option.

User context

Targeting rules let a flag return different values per user — for example, an admin-tools flag enabled only for role === 'admin', or beta-features enabled for plan === 'enterprise'. Targeting rules are configured server-side (Flagify dashboard or API); the SDK only forwards user attributes.

There are two valid patterns, depending on whether the process serves one user or many.

Pattern 1 — long-lived single-user client

For CLIs, edge workers, single-tenant background jobs, or React Server Components that build a fresh client per request, pass the user once via options.user. The client fetches all flag values already evaluated for that user at startup. After await flagify.ready(), isEnabled(), getValue(), and getVariant() return the targeted values from the local cache.

const flagify = new Flagify({
  projectKey: 'my-project',
  publicKey: 'pk_dev_abc123_xxxxxxxx',
  options: {
    user: {
      id: 'user-123',
      email: '[email protected]',
      role: 'admin',
      plan: 'pro',
    },
  },
});

await flagify.ready();
flagify.isEnabled('admin-tools'); // true if the targeting rule matches role === 'admin'

Pattern 2 — per-request evaluation (Express, Fastify, Next API routes)

In a typical multi-tenant web server, create the client once at startup with no options.user, and call await flagify.evaluate(key, user) per request. evaluate() calls the API, the server applies targeting rules, and returns the result.

Even without options.user, the local cache (and isEnabled / getValue / getVariant against it) already reflects catch-all and rollout rules thanks to the anonymous-context sync at startup. Use flagify.evaluate(key, user) only for rules that actually need user attributes.

import express from 'express';
import { Flagify } from '@flagify/node';

const flagify = new Flagify({
  projectKey: 'my-project',
  publicKey: 'pk_dev_abc123_xxxxxxxx',
  options: { realtime: true },
});
await flagify.ready();

const app = express();

app.get('/admin', async (req, res) => {
  // evaluate() makes a network call per request — always wrap in try/catch and
  // pick a safe fallback so an API blip doesn't 500 your handler.
  let allowed = false;
  try {
    const result = await flagify.evaluate('admin-tools', {
      id: req.user.id,
      role: req.user.role,
      email: req.user.email,
    });
    allowed = result.value === true;
  } catch (err) {
    console.warn('[flagify] evaluate failed, denying access by default', err);
  }

  if (!allowed) return res.status(403).end();
  res.render('admin');
});

The user object uses id (not userId) — the SDK serializes it to userId on the wire automatically.

{
  id: string                   // required
  email?: string
  role?: string
  group?: string
  geolocation?: { country?: string; region?: string; city?: string }
  [key: string]: unknown       // any custom attribute
}

evaluate() returns { key, value, reason } where reason is one of targeting_rule, rollout, default, no_match, or disabled. See the targeting concept docs for the full operator list and segment options.

Listening for flag changes

Subscribe to flag change events using the event emitter pattern:

const unsubscribe = flagify.onFlagChange((event) => {
  console.log(`Flag ${event.flagKey} was ${event.action}`);
});

// Later, unsubscribe when no longer needed
unsubscribe();

You can register multiple listeners. Each call to onFlagChange() returns an unsubscribe function.

Shutdown

Disconnect the realtime listener and clean up all resources:

flagify.destroy();

TypeScript types

All types are exported from @flagify/node:

import type {
  FlagifyOptions,
  FlagifyUser,
  FlagifyFlag,
  IFlagifyClient,
  FlagChangeEvent,
  EvaluateResult,
} from '@flagify/node';

FlagifyOptions

interface FlagifyOptions {
  projectKey: string;
  publicKey: string;
  secretKey?: string;
  options?: {
    user?: FlagifyUser;
    apiUrl?: string;
    staleTimeMs?: number;        // cache stale threshold — stale reads still return cached value, refetch fires in background
    realtime?: boolean;          // enable SSE streaming
    pollIntervalMs?: number;     // periodic flag sync (fallback when SSE is unavailable)
    sseIdleTimeoutMs?: number;   // SSE silence watchdog — aborts and reconnects on zombie TCP
    sseReconnectBaseMs?: number; // base delay for exponential backoff
    sseReconnectMaxMs?: number;  // cap for exponential backoff
  };
}

FlagifyUser

interface FlagifyUser {
  id: string;
  email?: string;
  role?: string;
  group?: string;
  geolocation?: {
    country?: string;
    region?: string;
    city?: string;
  };
  [key: string]: unknown; // custom attributes
}

FlagifyFlag

The shape of a flag returned by the API:

interface FlagifyFlag {
  key: string;
  name: string;
  value: boolean | string | number | Record<string, unknown>;
  description?: string;
  type: 'boolean' | 'string' | 'number' | 'json';
  defaultValue: boolean | string | number | Record<string, unknown>;
  offValue: boolean | string | number | Record<string, unknown>;
  enabled: boolean;
  rolloutPercentage?: number;
  targetingRules?: Array<{
    priority: number;
    segmentId?: string;
    valueOverride?: unknown;
    rolloutPercentage?: number;
    rolloutSalt?: string;
    enabled: boolean;
    matchType?: 'ALL' | 'ANY';
    conditions?: Array<{
      attribute: string;
      operator: 'equals' | 'not_equals' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'in' | 'not_in' | 'gt' | 'lt';
      value: unknown;
    }>;
  }>;
  variants?: Array<{
    key: string;
    value: boolean | string | number | Record<string, unknown>;
    weight: number;
  }>;
  createdAt: string;
  updatedAt: string;
}

IFlagifyClient

interface IFlagifyClient {
  getValue<T>(flagKey: string, fallback: T): T;
  isEnabled(flagKey: string): boolean;
  getVariant(flagKey: string, fallback: string): string;
  evaluate(flagKey: string, user: FlagifyUser): Promise<EvaluateResult>;
}

EvaluateResult

interface EvaluateResult {
  key: string;
  value: unknown;
  reason: 'targeting_rule' | 'rollout' | 'default' | 'disabled';
}