/Docs

Bun integration

Bun is a fast all-in-one JavaScript runtime that implements both the Node.js API surface and the Web Fetch API. You can use either @avsbhq/node (for the full server SDK with background polling, sticky bucketing, and decision logging) or @avsbhq/edge (for the lighter, fetch-only client). This guide covers both and shows how to run a hot-reloading development server with bun --hot.

Using @avsbhq/node (recommended for long-running servers)

1

Install

terminal
bash
1bun add @avsbhq/node @avsbhq/utils
2

Obtain your SDK key

Go to Settings → Environments in your A vs B project and copy the Server SDK key. Store it in a .env file or your deployment environment — never in source code.

.env
bash
1AVSB_SDK_KEY=sdk_production_...
3

Bootstrap the server

Initialise AvsbServerat module scope so it is shared across all requests in the same process. Bun's module cache ensures the singleton is reused even with --hot reloading.

server.ts
typescript
1import { AvsbServer } from '@avsbhq/node'
2
3const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! })
4
5// Wait for the first datafile fetch before accepting traffic
6await server.onReady()
7
8Bun.serve({
9 port: 3000,
10 async fetch(req: Request): Promise<Response> {
11 const userId = req.headers.get('x-user-id') ?? 'anon'
12 const plan = req.headers.get('x-user-plan') ?? 'free'
13
14 const avsb = server.forUser({ kind: 'user', key: userId, plan })
15
16 const flag = avsb.getFlag('checkout-v2', false)
17
18 return Response.json({ value: flag.value })
19 },
20})
4

Run the dev server with hot reload

terminal
bash
1bun --hot run server.ts
Tip
With --hot, Bun re-imports modules on file change but keeps the same process alive. Because AvsbServer is declared at module scope, it is re-initialised on each hot reload cycle. For development this is fine; in production you would run without --hot.
5

Read a flag

typescript
1// Boolean — T inferred as boolean
2const flag = avsb.getFlag('checkout-v2', false)
3
4// String
5const theme = avsb.getFlag('ui-theme', 'default')
6
7// JSON
8const pricing = avsb.getFlag('pricing-config', { tier: 'standard', seats: 1 })
9
10// Access the full Flag<T> object
11console.log(flag.source, flag.variationKey, flag.reasons)
6

Track an event

typescript
1avsb.track('purchase', {
2 value: 99.00,
3 properties: { sku: 'PRO-ANNUAL', currency: 'usd' },
4})
7

Identify a user

For multi-context evaluations — for example when you also want to target by organisation or device — build a multi-context object:

typescript
1const avsb = server.forUser({
2 kind: 'multi',
3 user: { kind: 'user', key: userId, plan },
4 organization: { kind: 'organization', key: orgId, tier: 'enterprise' },
5})

Using @avsbhq/edge (lighter option)

If you want the smallest possible footprint — for example in a Bun edge-proxy or serverless-style handler — use @avsbhq/edge instead. It has no background timers and loads the datafile once per process.

edge-server.ts
typescript
1import { AvsbEdgeClient } from '@avsbhq/edge'
2
3const avsb = new AvsbEdgeClient({ sdkKey: process.env.AVSB_SDK_KEY! })
4await avsb.onReady()
5
6Bun.serve({
7 port: 3000,
8 async fetch(req: Request): Promise<Response> {
9 const userId = req.headers.get('x-user-id') ?? 'anon'
10 const scoped = avsb.forUser({ kind: 'user', key: userId })
11 const flag = scoped.getFlag('new-banner', false)
12 return Response.json({ value: flag.value })
13 },
14})

Graceful shutdown

Bun propagates SIGTERM to the process. Register a handler to flush pending events before exiting:

typescript
1process.on('SIGTERM', async () => {
2 await server.flush()
3 await server.close()
4 process.exit(0)
5})

Testing

Bun ships its own test runner compatible with Jest matchers. Use @avsbhq/test to create mock clients without network access.

server.test.ts
typescript
1import { TestData, createMockClient } from '@avsbhq/test'
2import { describe, it, expect } from 'bun:test'
3
4const td = TestData.flag('checkout-v2')
5 .booleanFlag()
6 .variationForUser('u_pro', true)
7 .fallthroughVariation(false)
8
9const mock = createMockClient({ flags: [td.build()] })
10
11describe('flag evaluation', () => {
12 it('returns true for pro user', () => {
13 const flag = mock.forUser({ kind: 'user', key: 'u_pro' }).getFlag('checkout-v2', false)
14 expect(flag.value).toBe(true)
15 })
16
17 it('returns false as fallthrough', () => {
18 const flag = mock.forUser({ kind: 'user', key: 'other' }).getFlag('checkout-v2', false)
19 expect(flag.value).toBe(false)
20 })
21})

What's next