npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@avsbhq/node

v1.2.0

Published

Node.js server-side SDK for the [A vs B](https://app.avsb.cloud) platform.

Readme

@avsbhq/node

Node.js server-side SDK for the A vs B platform.

Evaluate feature flags, bucket users consistently, and track conversion events on the server. Includes Express and Fastify middleware, real-time datafile streaming over SSE, pluggable sticky bucketing (in-memory and Redis), multi-context evaluation, and per-request decision logs.


1. Install

npm install @avsbhq/node

Requires Node.js 18 or later.

No mandatory peer dependencies. Express and Fastify middleware are included but only activated when the corresponding framework is present. Install @avsbhq/utils for additional middleware (Hono, Koa, Lambda, Cloudflare Workers), OpenFeature integration, and warehouse-native decision sinks.


2. Quickstart

import { AvsbServer } from '@avsbhq/node'

const server = new AvsbServer({
  sdkKey: process.env.AVSB_SDK_KEY!,
})

const result = await server.onReady()
if (!result.success) {
  process.exit(1)  // or degrade gracefully: result.degraded === true means defaults are safe
}

// Evaluate per-request via forUser()
const user = server.forUser({ kind: 'user', key: 'u_123', plan: 'pro' })

const flag = user.getBoolFlag('new-checkout-flow', false)
if (flag.isEnabled()) {
  renderNewCheckout()
}

user.track('checkout_started', { value: 99.0 })

Instantiate AvsbServer once at app boot. The server holds the datafile in memory and refreshes it on a polling interval (or via SSE when streaming: true).


3. SDK keys

Get the SDK key for the environment you want to target from your A vs B project's Environments page. The key format is sdk_<environment>_<id> (e.g. sdk_production_clp1a2b3c4d5e6).

AVSB_SDK_KEY=sdk_production_...

When used server-side, the SDK key only ever needs to leave the server to hit the datafile CDN — it should never end up in client-rendered HTML, browser bundles, or browser-accessible config. Store it in environment variables only.

Server SDK keys cannot be rotated without a process restart; the new key must be set in the environment before the server boots (or trigger a rolling restart in your orchestrator).


4. Identity

The server SDK is stateless per request — it does not hold a persistent context. Pass the user context on each call using forUser() or as a context option on individual calls.

forUser(context) / createUserContext(context)

Creates a UserBoundClient scoped to the given context. All flag and track calls on the returned client use that context automatically. Carries its own per-request decision log.

const user = server.forUser({
  kind: 'user',
  key: req.user.id,
  plan: req.user.plan,
  country: req.user.country,
})

const flag = user.getBoolFlag('checkout-v2', false)
user.track('page_viewed', { properties: { page: '/checkout' } })

createUserContext is an ergonomic alias for forUser.

Per-call context

Pass context in the options object when not using forUser():

const flag = server.getBoolFlag('checkout-v2', false, {
  context: { kind: 'user', key: 'u_123', plan: 'pro' },
})

alias(previousContext, newContext)

Send an alias event linking two identities in the analytics pipeline. Typically called server-side when a user signs up or links an anonymous session to an account:

await server.alias(
  { kind: 'user', key: anonymousId },
  { kind: 'user', key: user.id }
)

5. Multi-context

Pass a multi-context to evaluate flags against multiple context kinds simultaneously:

const user = server.forUser({
  kind: 'multi',
  user: { kind: 'user', key: req.user.id, plan: req.user.plan },
  organization: { kind: 'organization', key: req.org.id, tier: req.org.tier },
})

const flag = user.getBoolFlag('enterprise-dashboard', false)
// The rule may target organization.tier and bucket on user.key simultaneously

6. Reading flags

Typed variants (recommended)

const darkMode    = user.getBoolFlag('dark-mode', false)
const theme       = user.getStringFlag('theme', 'light')
const maxItems    = user.getNumberFlag('max-results', 25)
const config      = user.getJsonFlag<{ timeout: number }>('api-config', { timeout: 5000 })

Each typed variant performs a runtime type check against the datafile's declared flag type. A mismatch logs a warning and returns the default.

Generic getFlag<T>

const flag = server.getFlag<boolean>('checkout-v2', false, {
  context: { kind: 'user', key: 'u_123' },
})

The Flag<T> object

flag.value          // T — the variation value
flag.isEnabled()    // true if source === 'rule' && value is truthy
flag.variationKey   // 'on' | 'off' | 'control' | null (null = default served)
flag.exists()       // false when source === 'not_found'
flag.source         // 'rule' | 'sticky' | 'holdout' | 'datafileOverride' | 'default' | ...
flag.ruleId         // which rule matched
flag.reasons        // string[] — evaluation path

getAllFlags(context, options?)

const all = server.getAllFlags({ kind: 'user', key: 'u_123' })
// Exposures are suppressed by default. Pass { fireExposures: true } to enable.

Forced decisions (QA / testing)

// Override for a specific user context key
server.setOverrideForUser('checkout-v2', 'u_qa', 'on')
server.setOverrideForUser('checkout-v2', 'u_qa', 'on', 'user')  // explicit kind

// Global override (all contexts)
server.setGlobalOverride('checkout-v2', 'on')

// Clear
server.clearOverrideForUser('checkout-v2', 'u_qa')
server.clearGlobalOverride('checkout-v2')
server.clearAllOverrides()

Per-request forced decisions (bound to a UserBoundClient):

const user = server.forUser(ctx)
user.setForcedDecision('checkout-v2', { variationKey: 'on' })
user.clearForcedDecision('checkout-v2')

7. Tracking events

user.track('purchase_completed', {
  value: 199.0,
  properties: { plan: 'annual', sku: 'PRO_ANNUAL' },
})

When called directly on server (not via forUser), include context:

server.track('purchase_completed', {
  value: 199.0,
  context: { kind: 'user', key: 'u_123' },
  properties: { plan: 'annual' },
})

Events are synchronous calls that queue the event for batch flushing. There is no await on track.


8. Error handling

logger option

import { AvsbServer, createLogger } from '@avsbhq/node'
import pino from 'pino'
import { createPinoTransport } from '@avsbhq/utils/log/pino'

const server = new AvsbServer({
  sdkKey: process.env.AVSB_SDK_KEY!,
  logger: createLogger({
    level: 'info',
    transports: [createPinoTransport({ logger: pino(), level: 'info' })],
  }),
})

onReady() error handling

const result = await server.onReady()
// result.success: boolean
// result.source: 'network' | 'bootstrap' | 'timeout' | 'error'
// result.degraded: true means init failed but defaults are served; polling continues
if (!result.success && !result.degraded) {
  throw result.error
}

onReady() never rejects — it always resolves. Check result.success to decide whether to proceed.

Event bus

const unsubscribe = server.on('error', (err) => {
  myLogger.error('SDK error', { error: err.message })
})

server.on('configUpdate', ({ publishedAt, reason }) => {
  myLogger.info('datafile refreshed', { publishedAt, reason })
})

9. SSR / hydration

Not applicable to server-side Node.js usage. For Next.js App Router SSR, see @avsbhq/next which provides getDatafile, evaluateFlagServer, and AvsbHydrator for the server-to-client bootstrap handoff.

For server-side rendering outside of Next.js, fetch the datafile via bootstrap to avoid an extra round-trip:

const server = new AvsbServer({
  sdkKey: process.env.AVSB_SDK_KEY!,
  bootstrap: cachedDatafile,  // pre-fetched FlagDatafile object
})
// onReady() resolves immediately with source: 'bootstrap'

10. Graceful shutdown

SIGTERM handler

process.on('SIGTERM', async () => {
  await server.close()  // abort in-flight fetches, drain events
  process.exit(0)
})

close() calls server.flush() internally, then aborts any open streaming connections and stops polling.

Manual flush

await server.flush()  // drain queued events without teardown

11. Testing

import { createMockClient, TestData } from '@avsbhq/test'

const td = TestData.flag('checkout-v2')
  .booleanFlag()
  .variationForUser('u_test', true)
  .fallthroughVariation(false)

const mock = createMockClient({ flags: [td.build()] })
// mock has the same interface as AvsbServer; inject it in tests

For assertion helpers:

import { recordEvaluations } from '@avsbhq/test'

const recorder = recordEvaluations(mock)
callCodeUnderTest(mock)
expect(recorder).toHaveEvaluated('checkout-v2')
  .withContext({ kind: 'user', key: 'u_test' })
  .withValue(true)

12. Framework integrations

Express

import express from 'express'
import { AvsbServer, expressMiddleware } from '@avsbhq/node'

const avsb = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! })
await avsb.onReady()

const app = express()
app.use(expressMiddleware(avsb, {
  contextFrom: (req) => ({
    kind: 'user',
    key: req.user?.id ?? 'anon',
    plan: req.user?.plan,
  }),
}))

app.get('/checkout', (req, res) => {
  const flag = req.avsb.getBoolFlag('new-checkout-flow', false)
  res.render(flag.isEnabled() ? 'checkout-v2' : 'checkout-v1')
})

expressMiddleware decorates each request with req.avsb — a UserBoundClient scoped to the context returned by contextFrom. It also opens an AsyncLocalStorage scope so getRequestClient() from @avsbhq/utils works anywhere in the call chain.

Fastify

import Fastify from 'fastify'
import { AvsbServer, fastifyPlugin } from '@avsbhq/node'

const avsb = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! })
await avsb.onReady()

const app = Fastify()
await app.register(fastifyPlugin, {
  server: avsb,
  contextFrom: (req) => ({
    kind: 'user',
    key: req.headers['x-user-id'] as string,
  }),
})

app.get('/features', (req, reply) => {
  const flag = req.avsb.getBoolFlag('new-feature', false)
  reply.send({ enabled: flag.isEnabled() })
})

Sticky bucketing

Keep users in the same variation across rule changes. In-memory is suitable for single-process deployments:

import { InMemoryStickyBucketService } from '@avsbhq/node'

const server = new AvsbServer({
  sdkKey: process.env.AVSB_SDK_KEY!,
  stickyBucketService: new InMemoryStickyBucketService(),
})

Redis for multi-process and multi-region deployments:

import { createClient } from 'redis'
import { RedisStickyBucketService } from '@avsbhq/node'

const redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()

const server = new AvsbServer({
  sdkKey: process.env.AVSB_SDK_KEY!,
  stickyBucketService: new RedisStickyBucketService({
    redis,
    prefix: 'avsb:sticky:',
  }),
})

DynamoDB and PostgreSQL adapters are available in @avsbhq/utils.

Real-time streaming

Enable SSE streaming to receive datafile updates the instant a flag changes — no polling delay:

const server = new AvsbServer({
  sdkKey: process.env.AVSB_SDK_KEY!,
  streaming: true,
})

Decision logs

Capture full evaluation reasons per request for debugging or audit:

import { createDecisionLog } from '@avsbhq/node'

const log = createDecisionLog()
const user = server.forUser({ kind: 'user', key: 'u_123' })
const flag = user.getBoolFlag('checkout-v2', false)
// The bound client's decision log captures all evaluations automatically:
const entries = user.getDecisionLog().entries
// [{ flagKey, value, variationKey, source, ruleId, ruleType, reasons, evaluatedAt }]

13. Migration

From LaunchDarkly Node

| LaunchDarkly Node | @avsbhq/node | |---|---| | init(sdkKey, options) | new AvsbServer({ sdkKey }) | | client.waitForInitialization() | await server.onReady() | | client.variation('key', context, default) | server.forUser(ctx).getBoolFlag('key', default).value | | client.variationDetail('key', context, default) | server.forUser(ctx).getFlag('key', default) | | client.track('event', context, { metricValue }) | server.forUser(ctx).track('event', { value }) | | client.identify(context) | not applicable — server is stateless; use forUser(ctx) | | client.allFlagsState(context) | server.getAllFlags(ctx) | | client.close() | await server.close() |

Key differences:

  • getFlag returns Flag<T> not a raw value. Access .value for the primitive.
  • track uses value (not metricValue) for the numeric metric.
  • The server is context-free at construction; all evaluation is per-request via forUser().
  • Multi-context is native — no ldUser adapter needed.

From Statsig Node

| Statsig Node | @avsbhq/node | |---|---| | Statsig.initialize(serverSecret) | new AvsbServer({ sdkKey }) | | Statsig.checkGate(user, 'gate') | server.forUser(ctx).getBoolFlag('gate', false).isEnabled() | | Statsig.getExperiment(user, 'exp').get('param', default) | server.forUser(ctx).getFlag('exp', default).value | | Statsig.logEvent(user, 'event', value, metadata) | server.forUser(ctx).track('event', { value, properties: metadata }) | | Statsig.flush() | await server.flush() |