@avsbhq/node 0.3.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,267 +2,467 @@
2
2
 
3
3
  Node.js server-side SDK for the [A vs B](https://app.avsb.cloud) platform.
4
4
 
5
- Evaluate feature flags, bucket users consistently, and track conversion events on the server. Includes optional Express and Fastify middleware, real-time datafile streaming over SSE, pluggable sticky bucketing (in-memory and Redis), and decision logs for debugging.
5
+ 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.
6
6
 
7
- ## Install
7
+ ---
8
+
9
+ ## 1. Install
8
10
 
9
11
  ```bash
10
12
  npm install @avsbhq/node
11
13
  ```
12
14
 
13
- ## Authentication
14
-
15
- You'll need a server SDK key from your A vs B project's environments page.
15
+ Requires Node.js 18 or later.
16
16
 
17
- 1. Log in to the [A vs B platform](https://app.avsb.cloud)
18
- 2. Open your project, go to **Environments**
19
- 3. Copy the **server** SDK key for the environment you want to use
17
+ 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.
20
18
 
21
- Server SDK keys grant full flag-read access. Store them in environment variables, never check them into source control.
19
+ ---
22
20
 
23
- ## Quick start
21
+ ## 2. Quickstart
24
22
 
25
23
  ```ts
26
- import { AvsBServer } from '@avsbhq/node'
24
+ import { AvsbServer } from '@avsbhq/node'
27
25
 
28
- const server = new AvsBServer({
26
+ const server = new AvsbServer({
29
27
  sdkKey: process.env.AVSB_SDK_KEY!,
30
28
  })
31
29
 
32
- await server.onReady()
30
+ const result = await server.onReady()
31
+ if (!result.success) {
32
+ process.exit(1) // or degrade gracefully: result.degraded === true means defaults are safe
33
+ }
33
34
 
34
- const showNewCheckout = server.evaluateFlag('new-checkout-flow', {
35
- userId: 'user_123',
36
- attributes: { plan: 'pro', country: 'US' },
37
- })
35
+ // Evaluate per-request via forUser()
36
+ const user = server.forUser({ kind: 'user', key: 'u_123', plan: 'pro' })
38
37
 
39
- await server.track('checkout_completed', {
40
- userId: 'user_123',
41
- revenue: 49.99,
42
- })
38
+ const flag = user.getBoolFlag('new-checkout-flow', false)
39
+ if (flag.isEnabled()) {
40
+ renderNewCheckout()
41
+ }
42
+
43
+ user.track('checkout_started', { value: 99.0 })
43
44
  ```
44
45
 
45
- Initialize once at app boot. The server holds an in-memory datafile and refreshes it on a polling interval (or via SSE if streaming is enabled).
46
+ 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`).
46
47
 
47
- ## API reference
48
+ ---
48
49
 
49
- ### `new AvsBServer(options)`
50
+ ## 3. SDK keys
50
51
 
51
- | Option | Type | Default | Description |
52
- |---|---|---|---|
53
- | `sdkKey` | `string` | — | **Required.** Server SDK key for the environment. |
54
- | `pollingInterval` | `number` | `60_000` | How often (ms) to refresh the datafile. Min `5_000`. |
55
- | `cdnHost` | `string` | `'https://cdn.avsb.cloud'` | CDN host for datafile fetches. |
56
- | `initTimeout` | `number` | `10_000` | Max ms to wait for initial datafile fetch. |
57
- | `bootstrap` | `FlagDatafile` | — | Pre-fetched datafile to skip the initial network request. |
58
- | `streaming` | `boolean` | `false` | Enable SSE streaming for real-time datafile updates. |
59
- | `streamingEndpoint` | `string` | derived | Custom SSE endpoint URL. |
60
- | `events` | `EventOptions` | defaults | Event tracking config (`flushInterval`, `batchSize`, `maxQueueSize`, `retryAttempts`). |
61
- | `stickyBucketService` | `StickyBucketService` | none | Pluggable sticky bucketing backend. |
62
- | `logger` | `Logger` | `noopLogger` | Custom logger (`debug` / `info` / `warn` / `error`). |
52
+ 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`).
63
53
 
64
- ### `server.onReady(): Promise<InitResult>`
54
+ ```bash
55
+ AVSB_SDK_KEY=sdk_production_...
56
+ ```
57
+
58
+ 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.
59
+
60
+ 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).
61
+
62
+ ---
65
63
 
66
- Resolves when the SDK has loaded its datafile.
64
+ ## 4. Identity
65
+
66
+ 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.
67
+
68
+ ### `forUser(context)` / `createUserContext(context)`
69
+
70
+ 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.
67
71
 
68
72
  ```ts
69
- const result = await server.onReady()
70
- if (!result.success) {
71
- console.error('SDK failed to initialize:', result.error)
72
- }
73
+ const user = server.forUser({
74
+ kind: 'user',
75
+ key: req.user.id,
76
+ plan: req.user.plan,
77
+ country: req.user.country,
78
+ })
79
+
80
+ const flag = user.getBoolFlag('checkout-v2', false)
81
+ user.track('page_viewed', { properties: { page: '/checkout' } })
73
82
  ```
74
83
 
75
- `InitResult.source` is one of `'network'`, `'bootstrap'`, `'cache'`, `'timeout'`, `'error'`.
84
+ `createUserContext` is an ergonomic alias for `forUser`.
76
85
 
77
- ### `server.evaluateFlag(key, userContext): unknown`
86
+ ### Per-call context
78
87
 
79
- Evaluate a flag for a specific user. The user context must include a `userId`.
88
+ Pass `context` in the options object when not using `forUser()`:
80
89
 
81
90
  ```ts
82
- const value = server.evaluateFlag('new-checkout-flow', {
83
- userId: 'user_123',
84
- attributes: { plan: 'pro' },
91
+ const flag = server.getBoolFlag('checkout-v2', false, {
92
+ context: { kind: 'user', key: 'u_123', plan: 'pro' },
85
93
  })
86
94
  ```
87
95
 
88
- `UserContext` fields:
89
- | Field | Type | Description |
90
- |---|---|---|
91
- | `userId` | `string` | **Required.** Bucketing identifier. |
92
- | `attributes` | `Record<string, unknown>` | Targeting attributes. |
93
- | `suppressExposure` | `boolean` | When `true`, don't fire an exposure event. |
94
- | `decisionLog` | `DecisionLog` | Collect detailed evaluation reasons into a log. |
95
- | `privateAttributes` | `string[]` | Attribute keys to strip from event payloads. |
96
+ ### `alias(previousContext, newContext)`
97
+
98
+ 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:
99
+
100
+ ```ts
101
+ await server.alias(
102
+ { kind: 'user', key: anonymousId },
103
+ { kind: 'user', key: user.id }
104
+ )
105
+ ```
106
+
107
+ ---
96
108
 
97
- ### `server.track(eventKey, trackContext): Promise<void>`
109
+ ## 5. Multi-context
98
110
 
99
- Send a conversion event.
111
+ Pass a multi-context to evaluate flags against multiple context kinds simultaneously:
100
112
 
101
113
  ```ts
102
- await server.track('purchase_completed', {
103
- userId: 'user_123',
104
- revenue: 99.99,
105
- properties: { plan: 'annual' },
114
+ const user = server.forUser({
115
+ kind: 'multi',
116
+ user: { kind: 'user', key: req.user.id, plan: req.user.plan },
117
+ organization: { kind: 'organization', key: req.org.id, tier: req.org.tier },
106
118
  })
119
+
120
+ const flag = user.getBoolFlag('enterprise-dashboard', false)
121
+ // The rule may target organization.tier and bucket on user.key simultaneously
107
122
  ```
108
123
 
109
- ### `SDK_VERSION`
124
+ ---
110
125
 
111
- The version of this SDK. Useful for telemetry and bug reports.
126
+ ## 6. Reading flags
127
+
128
+ ### Typed variants (recommended)
112
129
 
113
130
  ```ts
114
- import { SDK_VERSION } from '@avsbhq/node'
115
- console.log(SDK_VERSION)
131
+ const darkMode = user.getBoolFlag('dark-mode', false)
132
+ const theme = user.getStringFlag('theme', 'light')
133
+ const maxItems = user.getNumberFlag('max-results', 25)
134
+ const config = user.getJsonFlag<{ timeout: number }>('api-config', { timeout: 5000 })
116
135
  ```
117
136
 
118
- ## Framework integrations
137
+ Each typed variant performs a runtime type check against the datafile's declared flag type. A mismatch logs a warning and returns the default.
119
138
 
120
- ### Express
139
+ ### Generic `getFlag<T>`
121
140
 
122
141
  ```ts
123
- import express from 'express'
124
- import { AvsBServer, expressMiddleware } from '@avsbhq/node'
142
+ const flag = server.getFlag<boolean>('checkout-v2', false, {
143
+ context: { kind: 'user', key: 'u_123' },
144
+ })
145
+ ```
125
146
 
126
- const server = new AvsBServer({ sdkKey: process.env.AVSB_SDK_KEY! })
127
- await server.onReady()
147
+ ### The `Flag<T>` object
128
148
 
129
- const app = express()
130
- app.use(expressMiddleware(server, {
131
- userIdFrom: (req) => req.user?.id,
132
- attributesFrom: (req) => ({ plan: req.user?.plan }),
133
- }))
149
+ ```ts
150
+ flag.value // T — the variation value
151
+ flag.isEnabled() // true if source === 'rule' && value is truthy
152
+ flag.variationKey // 'on' | 'off' | 'control' | null (null = default served)
153
+ flag.exists() // false when source === 'not_found'
154
+ flag.source // 'rule' | 'sticky' | 'holdout' | 'datafileOverride' | 'default' | ...
155
+ flag.ruleId // which rule matched
156
+ flag.reasons // string[] — evaluation path
157
+ ```
134
158
 
135
- app.get('/checkout', (req, res) => {
136
- const showNew = req.avsb.getFlag('new-checkout-flow', false)
137
- res.render(showNew ? 'checkout-v2' : 'checkout-v1')
138
- })
159
+ ### `getAllFlags(context, options?)`
160
+
161
+ ```ts
162
+ const all = server.getAllFlags({ kind: 'user', key: 'u_123' })
163
+ // Exposures are suppressed by default. Pass { fireExposures: true } to enable.
139
164
  ```
140
165
 
141
- The middleware decorates each request with `req.avsb`, which exposes per-request `getFlag`, `getFlagDetails`, `getAllFlags`, and `track` methods scoped to the current user.
166
+ ### Forced decisions (QA / testing)
142
167
 
143
- ### Fastify
168
+ ```ts
169
+ // Override for a specific user context key
170
+ server.setOverrideForUser('checkout-v2', 'u_qa', 'on')
171
+ server.setOverrideForUser('checkout-v2', 'u_qa', 'on', 'user') // explicit kind
172
+
173
+ // Global override (all contexts)
174
+ server.setGlobalOverride('checkout-v2', 'on')
175
+
176
+ // Clear
177
+ server.clearOverrideForUser('checkout-v2', 'u_qa')
178
+ server.clearGlobalOverride('checkout-v2')
179
+ server.clearAllOverrides()
180
+ ```
181
+
182
+ Per-request forced decisions (bound to a `UserBoundClient`):
144
183
 
145
184
  ```ts
146
- import Fastify from 'fastify'
147
- import { AvsBServer, fastifyPlugin } from '@avsbhq/node'
185
+ const user = server.forUser(ctx)
186
+ user.setForcedDecision('checkout-v2', { variationKey: 'on' })
187
+ user.clearForcedDecision('checkout-v2')
188
+ ```
148
189
 
149
- const server = new AvsBServer({ sdkKey: process.env.AVSB_SDK_KEY! })
150
- await server.onReady()
190
+ ---
151
191
 
152
- const app = Fastify()
153
- await app.register(fastifyPlugin, {
154
- server,
155
- userIdFrom: (req) => req.headers['x-user-id'] as string,
192
+ ## 7. Tracking events
193
+
194
+ ```ts
195
+ user.track('purchase_completed', {
196
+ value: 199.0,
197
+ properties: { plan: 'annual', sku: 'PRO_ANNUAL' },
156
198
  })
157
199
  ```
158
200
 
159
- ## Sticky bucketing
201
+ When called directly on `server` (not via `forUser`), include `context`:
160
202
 
161
- Sticky bucketing keeps a user in the same variation across evaluations, even if rules or rollout percentages change.
203
+ ```ts
204
+ server.track('purchase_completed', {
205
+ value: 199.0,
206
+ context: { kind: 'user', key: 'u_123' },
207
+ properties: { plan: 'annual' },
208
+ })
209
+ ```
162
210
 
163
- ### In-memory (single process)
211
+ Events are synchronous calls that queue the event for batch flushing. There is no `await` on `track`.
212
+
213
+ ---
214
+
215
+ ## 8. Error handling
216
+
217
+ ### `logger` option
164
218
 
165
219
  ```ts
166
- import { AvsBServer, InMemoryStickyBucketService } from '@avsbhq/node'
220
+ import { AvsbServer, createLogger } from '@avsbhq/node'
221
+ import pino from 'pino'
222
+ import { createPinoTransport } from '@avsbhq/utils/log/pino'
167
223
 
168
- const server = new AvsBServer({
224
+ const server = new AvsbServer({
169
225
  sdkKey: process.env.AVSB_SDK_KEY!,
170
- stickyBucketService: new InMemoryStickyBucketService(),
226
+ logger: createLogger({
227
+ level: 'info',
228
+ transports: [createPinoTransport({ logger: pino(), level: 'info' })],
229
+ }),
171
230
  })
172
231
  ```
173
232
 
174
- ### Redis (multi-process / multi-region)
233
+ ### `onReady()` error handling
175
234
 
176
235
  ```ts
177
- import { createClient } from 'redis'
178
- import { AvsBServer, RedisStickyBucketService } from '@avsbhq/node'
236
+ const result = await server.onReady()
237
+ // result.success: boolean
238
+ // result.source: 'network' | 'bootstrap' | 'timeout' | 'error'
239
+ // result.degraded: true means init failed but defaults are served; polling continues
240
+ if (!result.success && !result.degraded) {
241
+ throw result.error
242
+ }
243
+ ```
179
244
 
180
- const redis = createClient({ url: process.env.REDIS_URL })
181
- await redis.connect()
245
+ `onReady()` never rejects — it always resolves. Check `result.success` to decide whether to proceed.
182
246
 
183
- const server = new AvsBServer({
184
- sdkKey: process.env.AVSB_SDK_KEY!,
185
- stickyBucketService: new RedisStickyBucketService({ redis, keyPrefix: 'avsb' }),
247
+ ### Event bus
248
+
249
+ ```ts
250
+ const unsubscribe = server.on('error', (err) => {
251
+ myLogger.error('SDK error', { error: err.message })
252
+ })
253
+
254
+ server.on('configUpdate', ({ publishedAt, reason }) => {
255
+ myLogger.info('datafile refreshed', { publishedAt, reason })
186
256
  })
187
257
  ```
188
258
 
189
- `RedisStickyBucketService` accepts any Redis client matching the `RedisLike` interface (just needs `get` and `set`).
259
+ ---
190
260
 
191
- ## Real-time updates
261
+ ## 9. SSR / hydration
192
262
 
193
- Enable SSE streaming to receive datafile updates the instant a flag changes on the platform — no polling delay.
263
+ 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.
264
+
265
+ For server-side rendering outside of Next.js, fetch the datafile via `bootstrap` to avoid an extra round-trip:
194
266
 
195
267
  ```ts
196
- const server = new AvsBServer({
268
+ const server = new AvsbServer({
197
269
  sdkKey: process.env.AVSB_SDK_KEY!,
198
- streaming: true,
270
+ bootstrap: cachedDatafile, // pre-fetched FlagDatafile object
271
+ })
272
+ // onReady() resolves immediately with source: 'bootstrap'
273
+ ```
274
+
275
+ ---
276
+
277
+ ## 10. Graceful shutdown
278
+
279
+ ### SIGTERM handler
280
+
281
+ ```ts
282
+ process.on('SIGTERM', async () => {
283
+ await server.close() // abort in-flight fetches, drain events
284
+ process.exit(0)
199
285
  })
200
286
  ```
201
287
 
202
- Or use the streaming client directly:
288
+ `close()` calls `server.flush()` internally, then aborts any open streaming connections and stops polling.
289
+
290
+ ### Manual flush
203
291
 
204
292
  ```ts
205
- import { createStreamingClient } from '@avsbhq/node'
293
+ await server.flush() // drain queued events without teardown
294
+ ```
206
295
 
207
- const stream = createStreamingClient({
208
- sdkKey: process.env.AVSB_SDK_KEY!,
209
- onUpdate: (datafile) => { /* refresh in-memory datafile */ },
296
+ ---
297
+
298
+ ## 11. Testing
299
+
300
+ ```ts
301
+ import { createMockClient, TestData } from '@avsbhq/test'
302
+
303
+ const td = TestData.flag('checkout-v2')
304
+ .booleanFlag()
305
+ .variationForUser('u_test', true)
306
+ .fallthroughVariation(false)
307
+
308
+ const mock = createMockClient({ flags: [td.build()] })
309
+ // mock has the same interface as AvsbServer; inject it in tests
310
+ ```
311
+
312
+ For assertion helpers:
313
+
314
+ ```ts
315
+ import { recordEvaluations } from '@avsbhq/test'
316
+
317
+ const recorder = recordEvaluations(mock)
318
+ callCodeUnderTest(mock)
319
+ expect(recorder).toHaveEvaluated('checkout-v2')
320
+ .withContext({ kind: 'user', key: 'u_test' })
321
+ .withValue(true)
322
+ ```
323
+
324
+ ---
325
+
326
+ ## 12. Framework integrations
327
+
328
+ ### Express
329
+
330
+ ```ts
331
+ import express from 'express'
332
+ import { AvsbServer, expressMiddleware } from '@avsbhq/node'
333
+
334
+ const avsb = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! })
335
+ await avsb.onReady()
336
+
337
+ const app = express()
338
+ app.use(expressMiddleware(avsb, {
339
+ contextFrom: (req) => ({
340
+ kind: 'user',
341
+ key: req.user?.id ?? 'anon',
342
+ plan: req.user?.plan,
343
+ }),
344
+ }))
345
+
346
+ app.get('/checkout', (req, res) => {
347
+ const flag = req.avsb.getBoolFlag('new-checkout-flow', false)
348
+ res.render(flag.isEnabled() ? 'checkout-v2' : 'checkout-v1')
210
349
  })
211
350
  ```
212
351
 
213
- ## Decision logs
352
+ `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.
214
353
 
215
- Capture full evaluation reasons for debugging or audit trails.
354
+ ### Fastify
216
355
 
217
356
  ```ts
218
- import { createDecisionLog } from '@avsbhq/node'
357
+ import Fastify from 'fastify'
358
+ import { AvsbServer, fastifyPlugin } from '@avsbhq/node'
219
359
 
220
- const log = createDecisionLog()
221
- const value = server.evaluateFlag('new-checkout-flow', {
222
- userId: 'user_123',
223
- decisionLog: log,
360
+ const avsb = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! })
361
+ await avsb.onReady()
362
+
363
+ const app = Fastify()
364
+ await app.register(fastifyPlugin, {
365
+ server: avsb,
366
+ contextFrom: (req) => ({
367
+ kind: 'user',
368
+ key: req.headers['x-user-id'] as string,
369
+ }),
224
370
  })
225
371
 
226
- console.log(log.entries)
227
- // [{ flagKey, value, variationKey, source, ruleId, ruleType, reasons, evaluatedAt, durationMs }]
372
+ app.get('/features', (req, reply) => {
373
+ const flag = req.avsb.getBoolFlag('new-feature', false)
374
+ reply.send({ enabled: flag.isEnabled() })
375
+ })
228
376
  ```
229
377
 
230
- ## Common patterns
378
+ ### Sticky bucketing
231
379
 
232
- ### Bootstrap once, evaluate per-request
380
+ Keep users in the same variation across rule changes. In-memory is suitable for single-process deployments:
233
381
 
234
382
  ```ts
235
- // startup.ts
236
- import { AvsBServer } from '@avsbhq/node'
383
+ import { InMemoryStickyBucketService } from '@avsbhq/node'
237
384
 
238
- export const avsb = new AvsBServer({ sdkKey: process.env.AVSB_SDK_KEY! })
239
- await avsb.onReady()
385
+ const server = new AvsbServer({
386
+ sdkKey: process.env.AVSB_SDK_KEY!,
387
+ stickyBucketService: new InMemoryStickyBucketService(),
388
+ })
240
389
  ```
241
390
 
391
+ Redis for multi-process and multi-region deployments:
392
+
242
393
  ```ts
243
- // route handler
244
- import { avsb } from './startup'
394
+ import { createClient } from 'redis'
395
+ import { RedisStickyBucketService } from '@avsbhq/node'
245
396
 
246
- const value = avsb.evaluateFlag('flag-key', { userId: req.user.id })
397
+ const redis = createClient({ url: process.env.REDIS_URL })
398
+ await redis.connect()
399
+
400
+ const server = new AvsbServer({
401
+ sdkKey: process.env.AVSB_SDK_KEY!,
402
+ stickyBucketService: new RedisStickyBucketService({
403
+ redis,
404
+ prefix: 'avsb:sticky:',
405
+ }),
406
+ })
247
407
  ```
248
408
 
249
- ### Suppress exposure for backend-only checks
409
+ DynamoDB and PostgreSQL adapters are available in `@avsbhq/utils`.
410
+
411
+ ### Real-time streaming
412
+
413
+ Enable SSE streaming to receive datafile updates the instant a flag changes — no polling delay:
250
414
 
251
415
  ```ts
252
- const value = server.evaluateFlag('feature-flag', {
253
- userId: 'user_123',
254
- suppressExposure: true,
416
+ const server = new AvsbServer({
417
+ sdkKey: process.env.AVSB_SDK_KEY!,
418
+ streaming: true,
255
419
  })
256
420
  ```
257
421
 
258
- ## Requirements
422
+ ### Decision logs
423
+
424
+ Capture full evaluation reasons per request for debugging or audit:
259
425
 
260
- - Node.js >= 18
261
- - No required peer dependencies. Express and Fastify are optional — only needed if you use the corresponding middleware.
426
+ ```ts
427
+ import { createDecisionLog } from '@avsbhq/node'
262
428
 
263
- ## Links
429
+ const log = createDecisionLog()
430
+ const user = server.forUser({ kind: 'user', key: 'u_123' })
431
+ const flag = user.getBoolFlag('checkout-v2', false)
432
+ // The bound client's decision log captures all evaluations automatically:
433
+ const entries = user.getDecisionLog().entries
434
+ // [{ flagKey, value, variationKey, source, ruleId, ruleType, reasons, evaluatedAt }]
435
+ ```
264
436
 
265
- - [Platform docs](https://app.avsb.cloud/docs)
266
- - [Client SDK (`@avsbhq/js`)](https://www.npmjs.com/package/@avsbhq/js)
267
- - [React SDK (`@avsbhq/react`)](https://www.npmjs.com/package/@avsbhq/react)
268
- - [CLI (`@avsbhq/cli`)](https://www.npmjs.com/package/@avsbhq/cli)
437
+ ---
438
+
439
+ ## 13. Migration
440
+
441
+ ### From LaunchDarkly Node
442
+
443
+ | LaunchDarkly Node | `@avsbhq/node` |
444
+ |---|---|
445
+ | `init(sdkKey, options)` | `new AvsbServer({ sdkKey })` |
446
+ | `client.waitForInitialization()` | `await server.onReady()` |
447
+ | `client.variation('key', context, default)` | `server.forUser(ctx).getBoolFlag('key', default).value` |
448
+ | `client.variationDetail('key', context, default)` | `server.forUser(ctx).getFlag('key', default)` |
449
+ | `client.track('event', context, { metricValue })` | `server.forUser(ctx).track('event', { value })` |
450
+ | `client.identify(context)` | not applicable — server is stateless; use `forUser(ctx)` |
451
+ | `client.allFlagsState(context)` | `server.getAllFlags(ctx)` |
452
+ | `client.close()` | `await server.close()` |
453
+
454
+ Key differences:
455
+ - `getFlag` returns `Flag<T>` not a raw value. Access `.value` for the primitive.
456
+ - `track` uses `value` (not `metricValue`) for the numeric metric.
457
+ - The server is context-free at construction; all evaluation is per-request via `forUser()`.
458
+ - Multi-context is native — no `ldUser` adapter needed.
459
+
460
+ ### From Statsig Node
461
+
462
+ | Statsig Node | `@avsbhq/node` |
463
+ |---|---|
464
+ | `Statsig.initialize(serverSecret)` | `new AvsbServer({ sdkKey })` |
465
+ | `Statsig.checkGate(user, 'gate')` | `server.forUser(ctx).getBoolFlag('gate', false).isEnabled()` |
466
+ | `Statsig.getExperiment(user, 'exp').get('param', default)` | `server.forUser(ctx).getFlag('exp', default).value` |
467
+ | `Statsig.logEvent(user, 'event', value, metadata)` | `server.forUser(ctx).track('event', { value, properties: metadata })` |
468
+ | `Statsig.flush()` | `await server.flush()` |