@avsbhq/edge
v1.0.0
Published
Edge-runtime SDK for the [A vs B](https://app.avsb.cloud) platform.
Readme
@avsbhq/edge
Edge-runtime SDK for the A vs B platform.
Single package, six runtimes. Evaluate feature flags and track events in Cloudflare Workers, Vercel Edge Functions, Fastly Compute, Netlify Edge Functions, Deno Deploy, Bun, and AWS Lambda@Edge. Built on @avsbhq/core; each runtime adapter handles the platform-specific KV caching, request lifecycles, and event flushing patterns.
1. Install
npm install @avsbhq/edgeNo mandatory peer dependencies. Each runtime adapter imports the platform-specific types it needs — install only what your runtime requires.
Supported runtimes:
- Cloudflare Workers (with KV/R2 datafile cache)
- Vercel Edge Functions (with Edge Config cache)
- Fastly Compute@Edge (with KV Store cache)
- Netlify Edge Functions
- Deno Deploy
- Bun
- AWS Lambda@Edge
2. Quickstart
Cloudflare Workers
// worker.ts
import { createCloudflareHandler } from '@avsbhq/edge/cloudflare'
const handler = createCloudflareHandler({
sdkKey: () => env.AVSB_SDK_KEY,
kvNamespace: (env) => env.AVSB_KV,
contextFrom: (req, env) => ({
kind: 'user' as const,
key: req.headers.get('cf-connecting-user') ?? 'anon',
}),
})
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const { client } = await handler.init(req, env, ctx)
const flag = client.getBoolFlag('new-feature', false)
const response = new Response(
JSON.stringify({ enabled: flag.isEnabled() }),
{ headers: { 'Content-Type': 'application/json' } }
)
// Flush events after response is sent
ctx.waitUntil(client.flushEvents())
return response
},
}Vercel Edge Functions
// api/feature-check/route.ts (Edge runtime)
import { createVercelEdgeHandler } from '@avsbhq/edge/vercel'
import { createClient } from '@vercel/edge-config'
export const runtime = 'edge'
const handler = createVercelEdgeHandler({
sdkKey: process.env.AVSB_SDK_KEY!,
edgeConfig: createClient(process.env.EDGE_CONFIG),
contextFrom: (req) => ({
kind: 'user' as const,
key: req.headers.get('x-user-id') ?? 'anon',
}),
})
export async function GET(req: Request) {
const { client } = await handler.init(req)
const flag = client.getBoolFlag('homepage-hero', false)
return Response.json({ variant: flag.variationKey })
}Fastly Compute@Edge
import { createFastlyHandler } from '@avsbhq/edge/fastly'
const handler = createFastlyHandler({
sdkKey: fastly.env.get('AVSB_SDK_KEY') ?? '',
kvStore: 'AVSB_KV',
contextFrom: (req) => ({
kind: 'user' as const,
key: req.headers.get('x-user-id') ?? 'anon',
}),
})
addEventListener('fetch', async (event) => {
const { client } = await handler.init(event.request)
const flag = client.getBoolFlag('my-flag', false)
event.respondWith(
new Response(JSON.stringify({ value: flag.value }))
)
})Netlify Edge Functions
// netlify/edge-functions/feature.ts
import { createNetlifyEdgeHandler } from '@avsbhq/edge/netlify'
const handler = createNetlifyEdgeHandler({
sdkKey: Deno.env.get('AVSB_SDK_KEY') ?? '',
contextFrom: (req) => ({
kind: 'user' as const,
key: new URL(req.url).searchParams.get('uid') ?? 'anon',
}),
})
export default async function (req: Request) {
const { client } = await handler.init(req)
const flag = client.getStringFlag('theme', 'light')
return Response.json({ theme: flag.value })
}
export const config = { path: '/api/theme' }Deno Deploy
import { createDenoHandler } from '@avsbhq/edge/deno'
const handler = createDenoHandler({
sdkKey: Deno.env.get('AVSB_SDK_KEY') ?? '',
contextFrom: (req) => ({
kind: 'user' as const,
key: req.headers.get('x-user-id') ?? 'anon',
}),
})
Deno.serve(async (req) => {
const { client } = await handler.init(req)
const flag = client.getBoolFlag('beta-feature', false)
return Response.json({ enabled: flag.isEnabled() })
})Bun
import { createBunHandler } from '@avsbhq/edge/bun'
const handler = createBunHandler({
sdkKey: Bun.env.AVSB_SDK_KEY ?? '',
contextFrom: (req) => ({
kind: 'user' as const,
key: req.headers.get('x-user-id') ?? 'anon',
}),
})
Bun.serve({
async fetch(req) {
const { client } = await handler.init(req)
const flag = client.getBoolFlag('new-ui', false)
return Response.json({ enabled: flag.isEnabled() })
},
})AWS Lambda@Edge
import { createLambdaEdgeHandler } from '@avsbhq/edge/lambda'
const handler = createLambdaEdgeHandler({
sdkKey: process.env.AVSB_SDK_KEY ?? '',
contextFrom: (event) => ({
kind: 'user' as const,
key: event.Records[0].cf.request.headers['x-user-id']?.[0]?.value ?? 'anon',
}),
})
export const lambdaHandler = async (event: CloudFrontRequestEvent) => {
const { client } = await handler.init(event)
const flag = client.getBoolFlag('origin-routing', false)
// Modify CloudFront request based on flag
const cf = event.Records[0].cf
if (flag.isEnabled()) cf.request.uri = '/v2' + cf.request.uri
return cf.request
}3. SDK keys
Use a server SDK key (sdk-server-...) — edge functions run server-side code. Store it in your runtime's secret store:
| Runtime | Secret storage |
|---|---|
| Cloudflare Workers | wrangler secret put AVSB_SDK_KEY |
| Vercel Edge | Vercel dashboard → Environment Variables |
| Fastly | fastly secret store |
| Netlify | Netlify dashboard → Environment Variables |
| Deno Deploy | Deno dashboard → Environment Variables |
| Bun | .env file or platform secret store |
| Lambda@Edge | AWS Secrets Manager or Lambda environment |
4. Identity
All adapters accept a contextFrom(req) function that extracts the EvalContext from the incoming request:
contextFrom: (req) => ({
kind: 'user' as const,
key: req.headers.get('x-user-id') ?? anonymousId(req),
country: req.cf?.country, // Cloudflare: geo from CF object
})For multi-context:
contextFrom: (req) => ({
kind: 'multi' as const,
user: {
kind: 'user' as const,
key: req.headers.get('x-user-id') ?? 'anon',
},
organization: {
kind: 'organization' as const,
key: req.headers.get('x-org-id') ?? 'none',
tier: req.headers.get('x-org-tier') ?? 'free',
},
})There is no identify() call in edge contexts — the context is immutable per request.
5. Multi-context
Pass a multi-context through contextFrom:
contextFrom: (req) => ({
kind: 'multi' as const,
user: { kind: 'user' as const, key: getUserId(req) },
organization: { kind: 'organization' as const, key: getOrgId(req) },
})The edge client evaluates rules against all context kinds simultaneously.
6. Reading flags
The AvsbEdgeClient exposes the same typed methods as @avsbhq/browser:
const boolFlag = client.getBoolFlag('dark-mode', false)
const strFlag = client.getStringFlag('theme', 'light')
const numFlag = client.getNumberFlag('max-results', 25)
const jsonFlag = client.getJsonFlag<{ timeout: number }>('api-config', { timeout: 5000 })
const genericFlag = client.getFlag<boolean>('checkout-v2', false)
const all = client.getAllFlags()All return Flag<T>. The flag object carries .value, .isEnabled(), .variationKey, .source, .reasons.
Edge runtimes have no persistent state between requests. The datafile is cached in the runtime-native KV store and fetched on cold start or TTL expiry.
7. Tracking events
client.track('page_viewed', {
value: 1,
properties: { path: new URL(req.url).pathname },
})
// Always flush after the response is sent
ctx.waitUntil(client.flushEvents()) // Cloudflare
// Other runtimes: await client.flushEvents() after responseEvents queued during request processing are flushed asynchronously after the response is sent. Never await the flush inside the response path — use ctx.waitUntil (CF) or fire-and-forget.
8. Error handling
Each adapter accepts an optional logger and onError callback:
const handler = createCloudflareHandler({
sdkKey: () => env.AVSB_SDK_KEY,
kvNamespace: (env) => env.AVSB_KV,
contextFrom: (req) => ({ kind: 'user', key: 'u_1' }),
logger: createLogger({ level: 'warn', transports: [consoleTransport()] }),
onError: (err, source) => {
// source: 'init' | 'eval' | 'track' | 'cache'
console.error('avsb error', source, err.message)
},
})If the datafile cache is stale or unavailable, the edge client serves defaults and logs a warning. It never throws — all errors are handled gracefully.
9. SSR / hydration
For server-side rendering in edge functions, evaluate flags in the request handler and pass the values to your rendered HTML or JSON response:
const flag = client.getBoolFlag('homepage-hero', false)
const html = renderToString(<App heroVariant={flag.variationKey ?? 'control'} />)For Next.js App Router edge middleware (not full rendering), use @avsbhq/next/middleware which is built on the same adapters.
10. Graceful shutdown
Edge runtimes have no persistent process — each request is a fresh invocation. There is no close() to call. Always flush events using the runtime's deferred-work mechanism:
// Cloudflare Workers
ctx.waitUntil(client.flushEvents())
// Vercel Edge (native fetch keepalive)
void client.flushEvents()
// Deno / Bun / Netlify
await client.flushEvents() // after response is sent11. Testing
import { AvsbEdgeClient } from '@avsbhq/edge'
import { createMockClient } from '@avsbhq/test'
const mock = createMockClient({
flags: [
TestData.flag('new-feature').booleanFlag().fallthroughVariation(true).build(),
],
})
// Test your handler with the mock injected
const response = await handleRequest(req, { avsbClient: mock })
expect(response.status).toBe(200)12. Migration
From LaunchDarkly Cloudflare
| LaunchDarkly Cloudflare | @avsbhq/edge/cloudflare |
|---|---|
| init(sdkKey, { kvNamespace }) | createCloudflareHandler({ sdkKey, kvNamespace, contextFrom }) |
| client.variation('key', ctx, default) | client.getBoolFlag('key', default).value |
| client.variationDetail('key', ctx, default) | client.getFlag('key', default) |
| client.flush(ctx) | ctx.waitUntil(client.flushEvents()) |
From Statsig Edge
| Statsig Edge | @avsbhq/edge |
|---|---|
| StatsigServer.initialize(key) | createCloudflareHandler({ sdkKey, contextFrom }) |
| checkGate(user, 'gate') | client.getBoolFlag('gate', false).isEnabled() |
| logEvent(user, 'event', value) | client.track('event', { value }) |
| flush() | ctx.waitUntil(client.flushEvents()) |
Key differences:
- All adapters are per-request — no singleton initialization needed.
- The datafile is cached in the runtime's native KV store, not fetched on every request.
- Multi-context is native — pass
{ kind: 'multi', ... }fromcontextFrom. - No
setIntervalorEventSource— edge environments have no persistent event loop.
