/Docs

Cloudflare Workers integration

Cloudflare Workers run in V8 isolates distributed across Cloudflare's global network. This guide shows how to evaluate feature flags with zero added latency by caching the A vs B datafile in Workers KV. By the end your Worker will read flags, track events, and flush event queues without holding up the response.

1

Install

terminal
bash
1npm install @avsbhq/edge
2

Obtain your SDK key

Open your A vs B project, go to Settings → Environments, and copy the Server SDK key. Add it to your Worker as an environment secret:

terminal
bash
1npx wrangler secret put AVSB_SDK_KEY
3

Declare the KV namespace in wrangler.toml

Create a KV namespace to cache the A vs B datafile so each Worker invocation reads from local KV rather than making an outbound HTTPS request.

wrangler.toml
toml
1name = "my-worker"
2main = "src/worker.ts"
3compatibility_date = "2024-09-23"
4
5[[kv_namespaces]]
6binding = "AVSB_DATAFILE_CACHE"
7id = "<your-kv-namespace-id>"
8preview_id = "<your-preview-kv-namespace-id>"
4

Bootstrap the edge client

Cloudflare Workers have no persistent background timers between requests, so the AvsbEdgeClient reads from KV on each cold start. The@avsbhq/edge/cloudflare subpath exports a createCloudflareHandler factory that wires KV caching, client init, and ctx.waitUntil-aware event flushing in one call.

src/worker.ts
typescript
1import { createCloudflareHandler } from '@avsbhq/edge/cloudflare'
2
3interface Env {
4 AVSB_SDK_KEY: string
5 AVSB_DATAFILE_CACHE: KVNamespace
6}
7
8export default {
9 fetch: createCloudflareHandler({
10 sdkKey: globalThis.__ENV?.AVSB_SDK_KEY ?? '',
11 kv: globalThis.__ENV?.AVSB_DATAFILE_CACHE,
12 contextFrom: (req) => ({
13 kind: 'user',
14 key: req.headers.get('cf-visitor-id') ?? 'anon',
15 }),
16 handler: async (req, client) => {
17 const flag = client.getFlag('new-homepage', false, {
18 kind: 'user',
19 key: req.headers.get('cf-visitor-id') ?? 'anon',
20 country: (req.cf?.country as string | undefined) ?? 'unknown',
21 })
22 return Response.json({ value: flag.value })
23 },
24 }),
25} satisfies ExportedHandler<Env>
Info
The factory accepts a structurally-typed KVNamespace binding for datafile caching with a 5-minute TTL, builds the per-request EvalContext via your contextFrom callback, and calls ctx.waitUntil(client.flushEvents()) automatically after your handler returns. If you need lower-level control, instantiate new AvsbEdgeClient(opts) from @avsbhq/edge directly.
KV cache miss on cold start
On the very first request after deploying (or after the KV entry expires) the edge client falls back to fetching the datafile from the A vs B CDN and writes it into KV for subsequent requests. Flag evaluation still works — default values are returned if the fetch fails.
5

Keep the datafile fresh

Register a datafile.published webhook in Settings → Webhooks. Your webhook receiver should write the new datafile to KV so all Workers pick up the change on their next cold start without waiting for the TTL to expire.

src/webhook-receiver.ts
typescript
1// Minimal example — add auth validation in production
2export default {
3 async fetch(request: Request, env: Env): Promise<Response> {
4 const body = await request.json<{ datafile: string }>()
5 await env.AVSB_DATAFILE_CACHE.put('avsb-datafile', body.datafile, {
6 expirationTtl: 3600,
7 })
8 return new Response('ok')
9 },
10}
6

Read a flag

typescript
1const flag = scoped.getFlag('new-homepage', false)
2// T is inferred as boolean from the defaultValue
3// flag.value — the result
4// flag.source — 'rule' | 'default' | 'not_found'
5// flag.variationKey — which variation (null if default)
7

Track an event

typescript
1scoped.track('page_view', {
2 properties: { path: new URL(request.url).pathname },
3})
4// Always pair track() calls with ctx.waitUntil(avsb.flush())
8

Identify a user

Build the EvalContext from whatever identity signal is available in the request — a cookie, a JWT claim, or a Cloudflare Access header.

typescript
1const jwt = request.headers.get('cf-access-jwt-assertion')
2const claims = jwt ? parseJwt(jwt) : null
3
4const scoped = avsb.forUser({
5 kind: 'user',
6 key: claims?.sub ?? crypto.randomUUID(),
7 plan: claims?.plan ?? 'free',
8 country: (request.cf?.country as string | undefined) ?? 'unknown',
9})

Graceful shutdown

Workers do not have a shutdown lifecycle — isolates are discarded silently. Always use ctx.waitUntil(avsb.flush()) to ensure queued exposure and tracking events are sent before the isolate is frozen. The flush() call is non-blocking from the perspective of the response.

Testing

worker.test.ts
typescript
1import { TestData, createMockClient } from '@avsbhq/test'
2import { describe, it, expect } from 'vitest'
3
4const td = TestData.flag('new-homepage')
5 .booleanFlag()
6 .variationForUser('u_1', true)
7 .fallthroughVariation(false)
8
9const mock = createMockClient({ flags: [td.build()] })
10
11describe('Worker flag evaluation', () => {
12 it('returns true for u_1', () => {
13 const flag = mock.forUser({ kind: 'user', key: 'u_1' }).getFlag('new-homepage', 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('new-homepage', false)
19 expect(flag.value).toBe(false)
20 })
21})

What's next