{"_id":"@avsbhq/edge","_rev":"4-ff7f69bfad18104d59bc04c4e063d014","name":"@avsbhq/edge","dist-tags":{"latest":"1.1.1"},"versions":{"1.0.0":{"name":"@avsbhq/edge","version":"1.0.0","_id":"@avsbhq/edge@1.0.0","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"homepage":"https://github.com/main1479/a-vs-b/tree/main/packages/avsb-edge#readme","bugs":{"url":"https://github.com/main1479/a-vs-b/issues"},"dist":{"shasum":"8b53ac35ae795f952363e310f75b7948d21d7149","tarball":"https://registry.npmjs.org/@avsbhq/edge/-/edge-1.0.0.tgz","fileCount":34,"integrity":"sha512-nM9gy95B+ejjOQdu1oLatMqBaVsz6jD17oD2g0Cq7VAdbneLp0f5Gw5sfuTxSDGGK8Iyfm/QEikkA72ZanwyTg==","signatures":[{"sig":"MEQCIGYBrJ4MMPwrCMPOEiuoma1uszvaGF2Ivd82BqubYlqbAiAR424D2lUzbYoTKylLJkeSHMyYenYSeGwSzw2lCXu/9Q==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":146986},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./bun":{"types":"./dist/adapters/bun.d.ts","import":"./dist/adapters/bun.js","require":"./dist/adapters/bun.cjs"},"./deno":{"types":"./dist/adapters/deno.d.ts","import":"./dist/adapters/deno.js","require":"./dist/adapters/deno.cjs"},"./fastly":{"types":"./dist/adapters/fastly.d.ts","import":"./dist/adapters/fastly.js","require":"./dist/adapters/fastly.cjs"},"./lambda":{"types":"./dist/adapters/lambdaEdge.d.ts","import":"./dist/adapters/lambdaEdge.js","require":"./dist/adapters/lambdaEdge.cjs"},"./vercel":{"types":"./dist/adapters/vercel.d.ts","import":"./dist/adapters/vercel.js","require":"./dist/adapters/vercel.cjs"},"./netlify":{"types":"./dist/adapters/netlify.d.ts","import":"./dist/adapters/netlify.js","require":"./dist/adapters/netlify.cjs"},"./cloudflare":{"types":"./dist/adapters/cloudflare.d.ts","import":"./dist/adapters/cloudflare.js","require":"./dist/adapters/cloudflare.cjs"}},"gitHead":"8cd6226e120fe67c198b2f1db0087cd38b4a4060","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","test:watch":"vitest"},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"repository":{"url":"git+https://github.com/main1479/a-vs-b.git","type":"git","directory":"packages/avsb-edge"},"_npmVersion":"10.9.2","description":"Edge-runtime SDK for the [A vs B](https://app.avsb.cloud) platform.","directories":{},"_nodeVersion":"22.14.0","dependencies":{"@avsbhq/core":"1.0.0","@avsbhq/utils":"1.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^3.0.0","typescript":"^5.5.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/edge_1.0.0_1780034347727_0.06974558440360967","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@avsbhq/edge","version":"1.0.1","_id":"@avsbhq/edge@1.0.1","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"homepage":"https://github.com/main1479/a-vs-b/tree/main/packages/avsb-edge#readme","bugs":{"url":"https://github.com/main1479/a-vs-b/issues"},"dist":{"shasum":"cbce5997157759a4f20cb729bf15af700a174c74","tarball":"https://registry.npmjs.org/@avsbhq/edge/-/edge-1.0.1.tgz","fileCount":34,"integrity":"sha512-u+w/W1Tj/egkyczwJS3qywiCCoPA6cPxq4f0BvfDs87kb3NfyDBoSrR9wuexyWvMQTr10f6Ab/KT4tD0L9MFFg==","signatures":[{"sig":"MEQCICRLXbKlmzEFnZuVw23t3tqPnrAG1bA35WPxxWmSA1ALAiBX02W9jPi9Y6WZZCZ0DeNg0uU0Nn5tuaol9SP1gOSg/g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":146961},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./bun":{"types":"./dist/adapters/bun.d.ts","import":"./dist/adapters/bun.js","require":"./dist/adapters/bun.cjs"},"./deno":{"types":"./dist/adapters/deno.d.ts","import":"./dist/adapters/deno.js","require":"./dist/adapters/deno.cjs"},"./fastly":{"types":"./dist/adapters/fastly.d.ts","import":"./dist/adapters/fastly.js","require":"./dist/adapters/fastly.cjs"},"./lambda":{"types":"./dist/adapters/lambdaEdge.d.ts","import":"./dist/adapters/lambdaEdge.js","require":"./dist/adapters/lambdaEdge.cjs"},"./vercel":{"types":"./dist/adapters/vercel.d.ts","import":"./dist/adapters/vercel.js","require":"./dist/adapters/vercel.cjs"},"./netlify":{"types":"./dist/adapters/netlify.d.ts","import":"./dist/adapters/netlify.js","require":"./dist/adapters/netlify.cjs"},"./cloudflare":{"types":"./dist/adapters/cloudflare.d.ts","import":"./dist/adapters/cloudflare.js","require":"./dist/adapters/cloudflare.cjs"}},"scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","test:watch":"vitest"},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"repository":{"url":"git+https://github.com/main1479/a-vs-b.git","type":"git","directory":"packages/avsb-edge"},"_npmVersion":"10.9.8","description":"Edge-runtime SDK for the [A vs B](https://app.avsb.cloud) platform.","directories":{},"_nodeVersion":"22.22.3","dependencies":{"@avsbhq/core":"1.4.0","@avsbhq/utils":"1.0.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^3.0.0","typescript":"^5.5.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/edge_1.0.1_1785346462962_0.3211757886136666","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@avsbhq/edge","version":"1.1.0","keywords":["avsb","feature-flags","ab-testing","experiments","edge","cloudflare-workers","vercel","sdk"],"license":"MIT","_id":"@avsbhq/edge@1.1.0","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"homepage":"https://github.com/avsbhq/a-vs-b/tree/main/packages/avsb-edge#readme","bugs":{"url":"https://github.com/avsbhq/a-vs-b/issues"},"dist":{"shasum":"fa761527600720c7f0e3850e69a60e175d6d10fe","tarball":"https://registry.npmjs.org/@avsbhq/edge/-/edge-1.1.0.tgz","fileCount":41,"integrity":"sha512-L1PWMYjswGpV2pLltxR4GyHaO69L2pqnr7i/wgnTmEF/g7wyeAapormOvgGLz4rmb5PjWeIGI+lLYE9ByoEu/w==","signatures":[{"sig":"MEYCIQCwlCnpwg6SrMJA758qEFQns0qp4BjfR3Im0okWUZcpZgIhAIFhD6Flsw7P/j/wHYliaddJj43Z7tbM2YIKAQIRXLEI","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":732323},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./bun":{"import":{"types":"./dist/adapters/bun.d.ts","default":"./dist/adapters/bun.js"},"require":{"types":"./dist/adapters/bun.d.cts","default":"./dist/adapters/bun.cjs"}},"./deno":{"import":{"types":"./dist/adapters/deno.d.ts","default":"./dist/adapters/deno.js"},"require":{"types":"./dist/adapters/deno.d.cts","default":"./dist/adapters/deno.cjs"}},"./fastly":{"import":{"types":"./dist/adapters/fastly.d.ts","default":"./dist/adapters/fastly.js"},"require":{"types":"./dist/adapters/fastly.d.cts","default":"./dist/adapters/fastly.cjs"}},"./lambda":{"import":{"types":"./dist/adapters/lambdaEdge.d.ts","default":"./dist/adapters/lambdaEdge.js"},"require":{"types":"./dist/adapters/lambdaEdge.d.cts","default":"./dist/adapters/lambdaEdge.cjs"}},"./vercel":{"import":{"types":"./dist/adapters/vercel.d.ts","default":"./dist/adapters/vercel.js"},"require":{"types":"./dist/adapters/vercel.d.cts","default":"./dist/adapters/vercel.cjs"}},"./netlify":{"import":{"types":"./dist/adapters/netlify.d.ts","default":"./dist/adapters/netlify.js"},"require":{"types":"./dist/adapters/netlify.d.cts","default":"./dist/adapters/netlify.cjs"}},"./cloudflare":{"import":{"types":"./dist/adapters/cloudflare.d.ts","default":"./dist/adapters/cloudflare.js"},"require":{"types":"./dist/adapters/cloudflare.d.cts","default":"./dist/adapters/cloudflare.cjs"}},"./package.json":"./package.json"},"gitHead":"5eed46d5944b522ba0aa34e82850c98bb91667fe","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","test:watch":"vitest"},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"repository":{"url":"git+https://github.com/avsbhq/a-vs-b.git","type":"git","directory":"packages/avsb-edge"},"_npmVersion":"10.9.8","description":"Edge SDK for A vs B feature flags and experiments, with adapters for Cloudflare Workers, Vercel, Fastly, Netlify, Deno, Bun, and Lambda@Edge.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.3","dependencies":{"@avsbhq/core":"^1.4.0","@avsbhq/utils":"^1.0.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^4.1.4","typescript":"^5.5.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/edge_1.1.0_1785971472174_0.23670801678739584","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"name":"@avsbhq/edge","version":"1.1.1","description":"Edge SDK for A vs B feature flags and experiments, with adapters for Cloudflare Workers, Vercel, Fastly, Netlify, Deno, Bun, and Lambda@Edge.","keywords":["avsb","feature-flags","ab-testing","experiments","edge","cloudflare-workers","vercel","sdk"],"license":"MIT","type":"module","sideEffects":false,"main":"dist/index.cjs","module":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./cloudflare":{"import":{"types":"./dist/adapters/cloudflare.d.ts","default":"./dist/adapters/cloudflare.js"},"require":{"types":"./dist/adapters/cloudflare.d.cts","default":"./dist/adapters/cloudflare.cjs"}},"./vercel":{"import":{"types":"./dist/adapters/vercel.d.ts","default":"./dist/adapters/vercel.js"},"require":{"types":"./dist/adapters/vercel.d.cts","default":"./dist/adapters/vercel.cjs"}},"./fastly":{"import":{"types":"./dist/adapters/fastly.d.ts","default":"./dist/adapters/fastly.js"},"require":{"types":"./dist/adapters/fastly.d.cts","default":"./dist/adapters/fastly.cjs"}},"./netlify":{"import":{"types":"./dist/adapters/netlify.d.ts","default":"./dist/adapters/netlify.js"},"require":{"types":"./dist/adapters/netlify.d.cts","default":"./dist/adapters/netlify.cjs"}},"./deno":{"import":{"types":"./dist/adapters/deno.d.ts","default":"./dist/adapters/deno.js"},"require":{"types":"./dist/adapters/deno.d.cts","default":"./dist/adapters/deno.cjs"}},"./bun":{"import":{"types":"./dist/adapters/bun.d.ts","default":"./dist/adapters/bun.js"},"require":{"types":"./dist/adapters/bun.d.cts","default":"./dist/adapters/bun.cjs"}},"./lambda":{"import":{"types":"./dist/adapters/lambdaEdge.d.ts","default":"./dist/adapters/lambdaEdge.js"},"require":{"types":"./dist/adapters/lambdaEdge.d.cts","default":"./dist/adapters/lambdaEdge.cjs"}},"./package.json":"./package.json"},"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/avsbhq/a-vs-b.git","directory":"packages/avsb-edge"},"homepage":"https://github.com/avsbhq/a-vs-b/tree/main/packages/avsb-edge#readme","bugs":{"url":"https://github.com/avsbhq/a-vs-b/issues"},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest"},"dependencies":{"@avsbhq/core":"^1.4.0","@avsbhq/utils":"^1.0.2"},"devDependencies":{"@types/node":"^20.0.0","tsup":"^8.0.0","typescript":"^5.5.0","vitest":"^4.1.4"},"_id":"@avsbhq/edge@1.1.1","gitHead":"d31d88fbf202f40041b4cb47f77882f454fa5ef8","_nodeVersion":"22.22.3","_npmVersion":"10.9.8","dist":{"integrity":"sha512-iEBi5tYsS8wh3ryVx5i1LERO3qe5LTkj3PNBPYzdUu3ck5ZqizjJChLVKgaY5FLEn3mXfKgbc5YSKn9PeAdhpw==","shasum":"3b2899d0ad33490c6356b98de12dfddf70884c68","tarball":"https://registry.npmjs.org/@avsbhq/edge/-/edge-1.1.1.tgz","fileCount":41,"unpackedSize":732459,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCkPkMG2A2ghB2c/5I9akkE5Yt8GBlx1yd/JOceNiBjbAIhAMD2NGSbPNq/gGHRi8nm7tdcV9vNOEY6eBgxXdLG/6bm"}]},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"directories":{},"maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/edge_1.1.1_1786795830471_0.9879109076737542"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-29T05:59:07.534Z","modified":"2026-08-15T12:10:30.820Z","1.0.0":"2026-05-29T05:59:07.873Z","1.0.1":"2026-07-29T17:34:23.152Z","1.1.0":"2026-08-05T23:11:12.346Z","1.1.1":"2026-08-15T12:10:30.619Z"},"bugs":{"url":"https://github.com/avsbhq/a-vs-b/issues"},"license":"MIT","homepage":"https://github.com/avsbhq/a-vs-b/tree/main/packages/avsb-edge#readme","keywords":["avsb","feature-flags","ab-testing","experiments","edge","cloudflare-workers","vercel","sdk"],"repository":{"type":"git","url":"git+https://github.com/avsbhq/a-vs-b.git","directory":"packages/avsb-edge"},"description":"Edge SDK for A vs B feature flags and experiments, with adapters for Cloudflare Workers, Vercel, Fastly, Netlify, Deno, Bun, and Lambda@Edge.","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"readme":"# @avsbhq/edge\n\nFeature flags and experiments at the edge, for the [A vs B](https://app.avsb.cloud) platform.\n\nOne package, seven runtimes: Cloudflare Workers, Vercel Edge Functions, Fastly Compute, Netlify Edge Functions, Deno Deploy, Bun, and AWS Lambda@Edge. Built on `@avsbhq/core`. Each adapter handles that platform's request lifecycle, its datafile cache, and its deferred-work mechanism.\n\nEvery code block on this page is written to compile against the real types in `src/`.\n\n---\n\n## 1. Install\n\n```bash\nnpm install @avsbhq/edge\n```\n\nNo mandatory peer dependencies. Platform types are declared structurally inside each adapter, so you never install `@cloudflare/workers-types`, `@vercel/edge-config`, `@fastly/js-compute`, or `@types/aws-lambda` just to use this package.\n\n---\n\n## 2. Quickstart\n\nEvery adapter follows the same shape: you give it an SDK key and a handler, it gives you back the runtime's native handler signature. The client inside your handler is already initialised and already bound to the visitor from the request, so `client.getBoolFlag('key', false)` needs no context argument.\n\n### Cloudflare Workers\n\nCloudflare exposes secrets and KV bindings only on the `env` argument of `fetch`, so `sdkKey` and `kv` also accept a function of `env`.\n\n```ts\nimport { createCloudflareHandler } from '@avsbhq/edge/cloudflare';\nimport type { KVNamespace } from '@avsbhq/edge/cloudflare';\n\ninterface Env {\n  AVSB_SDK_KEY: string;\n  AVSB_KV: KVNamespace;\n}\n\nexport default {\n  fetch: createCloudflareHandler<Env>({\n    sdkKey: (env) => env.AVSB_SDK_KEY,\n    kv: (env) => env.AVSB_KV,\n    handler: async (req, client) => {\n      const checkout = client.getBoolFlag('new-checkout', false);\n      return Response.json({ enabled: checkout.isEnabled() });\n    },\n  }),\n};\n```\n\n### Vercel Edge Functions\n\n```ts\nimport { createVercelHandler } from '@avsbhq/edge/vercel';\nimport { createClient } from '@vercel/edge-config';\n\nexport const runtime = 'edge';\n\nconst avsb = createVercelHandler({\n  sdkKey: process.env.AVSB_SDK_KEY ?? '',\n  edgeConfig: createClient(process.env.EDGE_CONFIG),\n  handler: async (req, client) => {\n    const hero = client.getStringFlag('homepage-hero', 'control');\n    return Response.json({ variant: hero.value });\n  },\n});\n\nexport function GET(req: Request): Promise<Response> {\n  return avsb(req);\n}\n```\n\n### Fastly Compute\n\nThe handler takes the whole fetch event, because Fastly hangs geolocation and the client address off `event.client`. `kvStore` takes a thunk, because Fastly forbids constructing platform resources during global initialisation.\n\n```ts\n/// <reference types=\"@fastly/js-compute\" />\n// docs-example: not typechecked here, because Fastly's global types ship with\n// @fastly/js-compute, which this repo does not install, so addEventListener\n// resolves to the DOM's own Event here.\nimport { createFastlyHandler } from '@avsbhq/edge/fastly';\nimport { KVStore } from 'fastly:kv-store';\n\nconst avsb = createFastlyHandler({\n  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',\n  kvStore: () => new KVStore('avsb-datafiles'),\n  handler: async (req, client) => {\n    const flag = client.getBoolFlag('new-checkout', false);\n    return Response.json({ enabled: flag.isEnabled() });\n  },\n});\n\naddEventListener('fetch', (event) => event.respondWith(avsb(event)));\n```\n\n### Netlify Edge Functions\n\n```ts\n// docs-example: not typechecked here, because the Netlify global ships with\n// the Netlify Edge runtime rather than from npm, so this repo has nothing to\n// check Netlify.env against.\nimport { createNetlifyHandler } from '@avsbhq/edge/netlify';\n\nexport default createNetlifyHandler({\n  sdkKey: Netlify.env.get('AVSB_SDK_KEY') ?? '',\n  handler: async (req, client) => {\n    const theme = client.getStringFlag('theme', 'light');\n    return Response.json({ theme: theme.value });\n  },\n});\n\nexport const config = { path: '/api/theme' };\n```\n\n### Deno Deploy\n\n```ts\n// docs-example: not typechecked here, because Deno's global types ship with\n// the deno CLI rather than from npm, so this repo has nothing to check\n// Deno.openKv, Deno.env and Deno.serve against.\nimport { createDenoHandler } from '@avsbhq/edge/deno';\n\nconst kv = await Deno.openKv();\n\nDeno.serve(\n  createDenoHandler({\n    sdkKey: Deno.env.get('AVSB_SDK_KEY') ?? '',\n    kv,\n    handler: async (req, client) => {\n      const dark = client.getBoolFlag('dark-mode', false);\n      return Response.json({ darkMode: dark.value });\n    },\n  }),\n);\n```\n\n### Bun\n\n```ts\n// docs-example: not typechecked here, because Bun's global types ship with the\n// bun runtime rather than from npm, so this repo has nothing to check Bun.serve\n// and Bun.env against.\nimport { createBunHandler } from '@avsbhq/edge/bun';\n\nBun.serve({\n  port: 3000,\n  fetch: createBunHandler({\n    sdkKey: Bun.env.AVSB_SDK_KEY ?? '',\n    handler: async (req, client) => {\n      const pageSize = client.getNumberFlag('page-size', 25);\n      return Response.json({ pageSize: pageSize.value });\n    },\n  }),\n});\n```\n\n### AWS Lambda@Edge\n\nReturn a `Response` to answer from the edge, or return nothing to let CloudFront continue to the origin with the request as you left it.\n\n```ts\nimport { createLambdaEdgeHandler } from '@avsbhq/edge/lambda';\n\nexport const handler = createLambdaEdgeHandler({\n  sdkKey: process.env.AVSB_SDK_KEY ?? '',\n  handler: async (req, client, cfRequest) => {\n    const redesign = client.getBoolFlag('hero-redesign', false);\n    if (redesign.isEnabled()) {\n      cfRequest.uri = `/v2${cfRequest.uri}`;\n    }\n  },\n});\n```\n\n---\n\n## 3. SDK keys\n\nUse the environment SDK key for the environment you are serving, shaped `sdk_<environment>_<id>`. Copy it from app.avsb.cloud, then Environments in the project sidebar.\n\nThe key is validated at construction. A value that does not look like an SDK key logs an error naming what it looks like instead (a pasted URL, a personal access token, a truncated copy) and the client keeps going, so a 404 on the next datafile request is explained rather than mysterious.\n\n| Runtime            | Where the key lives                                                              |\n| ------------------ | -------------------------------------------------------------------------------- |\n| Cloudflare Workers | `wrangler secret put AVSB_SDK_KEY`, read via `sdkKey: (env) => env.AVSB_SDK_KEY` |\n| Vercel Edge        | Project settings, Environment Variables                                          |\n| Fastly Compute     | `fastly.env.get('AVSB_SDK_KEY')` or a config store                               |\n| Netlify Edge       | Site settings, Environment Variables                                             |\n| Deno Deploy        | Project settings, Environment Variables                                          |\n| Bun                | `.env` or your platform's secret store                                           |\n| Lambda@Edge        | Bundled at deploy time (Lambda@Edge does not support environment variables)      |\n\n---\n\n## 4. Identity\n\nEach adapter builds an evaluation context from the request. You do not have to write one.\n\nThe default resolves the visitor in this order:\n\n1. The `x-avsb-visitor-id` request header.\n2. The A vs B snippet's `_avsb_visitor` cookie, when Web Experiments also runs on this site. Reading it means the edge SDK and the snippet report the same visitor, so both surfaces join in results.\n3. Nothing. The context key is the empty string and the SDK warns once, naming both sources and the option to set.\n\nThe client IP is deliberately never used as the bucketing key. The bucketing key becomes `visitorId` on every exposure row, so bucketing on IP would write a personal identifier into analytics storage for every visitor. IP-derived country and city are used as targeting attributes, because those are not stable per-person identifiers.\n\n### What each adapter extracts\n\nAttribute names match the visitor-data factories in `@avsbhq/utils`, so an audience authored once targets the snippet and the edge alike. An attribute the platform does not provide is omitted, never written as an empty string, so an `exists` audience condition can tell \"no geo on this platform\" from \"geo says empty\".\n\n| Adapter      | Identity                          | Geo source                                                                             | Geo attributes                                                              | Extra                      |\n| ------------ | --------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------- |\n| `cloudflare` | header, cookie                    | `request.cf`, falling back to the `cf-ipcountry` header                                | country, region, city, continent, postalCode, timezone, latitude, longitude | colo, asn                  |\n| `vercel`     | header, cookie                    | `x-vercel-ip-*` headers (city is percent-decoded)                                      | country, region, city, timezone, latitude, longitude                        | none                       |\n| `fastly`     | header, cookie                    | `event.client.geo` (snake_case mapped to the shared names)                             | country, region, city, continent, postalCode, latitude, longitude           | none                       |\n| `netlify`    | header, cookie, `context.cookies` | `context.geo` (country and subdivision flattened)                                      | country, region, city, timezone, latitude, longitude                        | none                       |\n| `deno`       | header, cookie                    | none, Deno Deploy exposes no geo                                                       | none                                                                        | region, from `DENO_REGION` |\n| `bun`        | header, cookie                    | none, Bun is a runtime and not a network                                               | none                                                                        | none                       |\n| `lambda`     | header, cookie                    | `cloudfront-viewer-*` headers, present only when the distribution policy forwards them | country, region, city, postalCode, timezone, latitude, longitude            | none                       |\n\nEvery adapter also sets `url`, `path`, `host` from the request URL and `userAgent`, `language`, `referrer` from headers.\n\n### Writing your own\n\n`contextFrom` is optional on every adapter. Supply one to replace the default, and build on the exported helpers so you keep the platform data:\n\n```ts\nimport { createCloudflareHandler, cloudflareContextFrom } from '@avsbhq/edge/cloudflare';\n\nexport default {\n  fetch: createCloudflareHandler({\n    sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',\n    contextFrom: (req) => ({\n      ...cloudflareContextFrom(req),\n      key: req.headers.get('x-user-id') ?? 'anon',\n      plan: req.headers.get('x-account-plan') ?? 'free',\n    }),\n    handler: async (req, client) => Response.json({ ok: client.isReady() }),\n  }),\n};\n```\n\nOn Fastly, Deno, and Bun this is also how you add geo:\n\n```ts\nimport { createFastlyHandler, fastlyContextFrom } from '@avsbhq/edge/fastly';\n\nconst avsb = createFastlyHandler({\n  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',\n  contextFrom: (event) => ({\n    ...fastlyContextFrom(event),\n    cartValue: Number(event.request.headers.get('x-cart-value') ?? 0),\n  }),\n  handler: async (req, client) => Response.json({ ok: client.isReady() }),\n});\n```\n\n### Multi-context\n\nReturn a multi-context from `contextFrom` to target on more than one identity. Bucketing uses the `user` sub-context.\n\n```ts\nimport { readVisitorId } from '@avsbhq/edge';\nimport type { EvalContext } from '@avsbhq/edge';\nimport { createVercelHandler } from '@avsbhq/edge/vercel';\n\nfunction contextFrom(req: Request): EvalContext {\n  return {\n    kind: 'multi',\n    user: { kind: 'user', key: readVisitorId(req.headers) ?? '' },\n    organization: {\n      kind: 'organization',\n      key: req.headers.get('x-org-id') ?? 'none',\n      tier: req.headers.get('x-org-tier') ?? 'free',\n    },\n  };\n}\n\nexport const handler = createVercelHandler({\n  sdkKey: process.env.AVSB_SDK_KEY ?? '',\n  contextFrom,\n  handler: async (req, client) => Response.json({ ok: client.isReady() }),\n});\n```\n\nThere is no `identify()` at the edge: the context is fixed for the life of one request.\n\n---\n\n## 5. Reading flags\n\nThe rest of this page names the package's own types, and shows methods on the client your handler receives. Both come from here:\n\n```ts\nimport { AvsbEdgeClient } from '@avsbhq/edge';\nimport type {\n  EdgeErrorSource,\n  EdgeLogLevel,\n  EvalContext,\n  EvaluationSource,\n  Flag,\n  FlagDatafile,\n  GetAllFlagsOptions,\n  GetFlagOptions,\n  InitResult,\n  Logger,\n  TrackPayload,\n  UnifiedStorageAdapter,\n} from '@avsbhq/edge';\nimport type { RuleType } from '@avsbhq/core';\n\nconst client = new AvsbEdgeClient({ sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn' });\n```\n\n```ts\ngetFlag<T>(flagKey: string, defaultValue: T, context?: EvalContext, options?: GetFlagOptions): Flag<T>\ngetBoolFlag(flagKey: string, defaultValue: boolean, context?: EvalContext, options?: GetFlagOptions): Flag<boolean>\ngetStringFlag(flagKey: string, defaultValue: string, context?: EvalContext, options?: GetFlagOptions): Flag<string>\ngetNumberFlag(flagKey: string, defaultValue: number, context?: EvalContext, options?: GetFlagOptions): Flag<number>\ngetJsonFlag<T>(flagKey: string, defaultValue: T, context?: EvalContext, options?: GetFlagOptions): Flag<T>\ngetAllFlags(context?: EvalContext, options?: GetAllFlagsOptions): Record<string, Flag>\n```\n\n`context` is optional because the adapter already bound one. Pass it to evaluate one call against a different identity without changing the bound context.\n\nEvery read returns a frozen `Flag<T>`:\n\n```ts\ninterface Flag<T = unknown> {\n  readonly value: T;\n  readonly variationKey: string | null;\n  readonly source: EvaluationSource;\n  readonly ruleId: string | null;\n  readonly ruleType: RuleType | null;\n  readonly reasons: string[];\n  readonly evaluatedAt: number;\n  readonly durationMicros: number;\n  isEnabled(): boolean;\n  exists(): boolean;\n}\n```\n\n`source` says where the value came from:\n\n| Value              | Meaning                                                        |\n| ------------------ | -------------------------------------------------------------- |\n| `rule`             | a targeting rule or experiment matched                         |\n| `holdout`          | this visitor is held out of the flag                           |\n| `bandit`           | a bandit rule chose the action                                 |\n| `datafileOverride` | an operator pinned this visitor in the dashboard               |\n| `runtimeOverride`  | your code pinned it at runtime                                 |\n| `sticky`           | a stored assignment was reused                                 |\n| `default`          | no rule matched, the flag's default variation was served       |\n| `disabled`         | the flag is turned off, its default variation was served       |\n| `not_found`        | the datafile loaded and this key is not in it                  |\n| `not_ready`        | `init()` had not finished, so your `defaultValue` was returned |\n\n`isEnabled()` is true only when the value came from a real decision (`rule`, `holdout`, `bandit`, `runtimeOverride`, `datafileOverride`, `sticky`) and is truthy. It is always false for `default`, `not_found`, `disabled`, and `not_ready`.\n\n`exists()` is false for `not_found` and for `not_ready`, because with no datafile the question is not yet answerable.\n\n### Typed getters and type mismatches\n\nA typed getter checks the type the dashboard declares for the flag. On a mismatch it returns your default with `source: 'not_found'` and logs a line naming both types. It never coerces:\n\n```ts\n// Flag \"theme\" is declared as a string in the dashboard.\nconst size = client.getNumberFlag('theme', 25);\n// size.value === 25, size.source === 'not_found', and one warning is logged.\n```\n\n`getJsonFlag<T>` types the value against `T` for you. It does not validate the shape field by field: the runtime check only rejects `undefined`.\n\n### Bulk reads\n\n`getAllFlags()` fires no exposure events by default. A bulk read is not a decision served to a visitor, and one exposure per flag would attribute every visitor to every experiment at once. Pass `{ fireExposures: true }` when you genuinely are serving all of them.\n\n---\n\n## 6. Tracking events\n\n```ts\ntrack(eventKey: string, payload?: TrackPayload): void\nflushEvents(): Promise<void>\n```\n\n```ts\ninterface TrackPayload {\n  /** Money in decimal MAJOR units of the project currency. Column `revenue`. */\n  revenue?: number;\n  /** Numeric metric value for average-value metrics. Column `value`. */\n  value?: number;\n  properties?: Record<string, unknown>;\n  context?: EvalContext;\n}\n```\n\n```ts\nclient.track('signup_completed'); // a count\nclient.track('purchase', { revenue: 49.99 }); // money\nclient.track('items_added', { value: 3 }); // a quantity\nclient.track('checkout_completed', { revenue: 99.5, value: 2 }); // both\n```\n\n`revenue` and `value` are two columns end to end, exactly as in the browser and node SDKs, so one conversion can carry money, a quantity, or both. Until 1.x this client sent `payload.value` into the `revenue` column and could never write `value` at all (breaking change B36): a quantity was recorded as money, and an edge-tracked average-value metric read empty. If you were relying on that, rename the field to `revenue`.\n\n`payload.properties` is accepted for cross-runtime symmetry but is **not stored** on tracked events, because the metric ingestion body has no properties field. The SDK warns once naming `revenue` and `value` as the places for numbers, rather than shipping something that looks delivered and is gone. Exposure events do carry properties.\n\nExposures are automatic: reading a flag whose decision came from an experiment queues one.\n\n### Flushing\n\n`flushEvents()` sends everything the request produced: queued exposures, queued tracked events, and any deferred cache write or heartbeat. Each adapter already calls it correctly for its runtime, so you only call it yourself if you build the client by hand.\n\n| Runtime                   | How the flush runs                                                 |\n| ------------------------- | ------------------------------------------------------------------ |\n| Cloudflare                | `ctx.waitUntil`, after the response                                |\n| Vercel                    | `waitUntil` when a FetchEvent is passed, otherwise awaited         |\n| Netlify                   | `context.waitUntil` when present, otherwise awaited                |\n| Fastly, Deno, Lambda@Edge | awaited before the response, because the instance ends with it     |\n| Bun                       | started without blocking, because Bun's loop outlives the response |\n\nAn edge invocation ends with its request, so a batch that cannot be delivered is lost rather than retried later. The failure log says exactly that, naming the endpoint, the status, and how many events went with it. One retry inside the request is on by default (`eventRetryAttempts`), and every attempt sends byte-identical event ids so ingestion deduplicates instead of double-counting.\n\n### Where events go\n\nEndpoints come from the datafile's `collectEndpoint`, composed through the endpoint contract in `@avsbhq/core`:\n\n| What           | Path                                             |\n| -------------- | ------------------------------------------------ |\n| Exposures      | `POST {collectEndpoint}/v1/collect/batch`        |\n| Tracked events | `POST {collectEndpoint}/v1/collect/metric-batch` |\n| Liveness       | `POST {datafile.heartbeatEndpoint}`              |\n\nThe SDK key rides in the JSON body envelope, because the collect paths are shared by every key. Datafile reads are keyless by path: the key is the path segment, with no header, no query string, and no `Authorization`.\n\n---\n\n## 7. Lifecycle and readiness\n\n```ts\ninit(): Promise<InitResult>\ngetInitResult(): InitResult | null\nisReady(): boolean\ngetDatafile(): FlagDatafile | null\ngetContext(): EvalContext | undefined\n```\n\n```ts\ninterface InitResult {\n  success: boolean;\n  source: 'network' | 'bootstrap' | 'timeout' | 'error';\n  error?: Error;\n  degraded?: boolean;\n}\n```\n\n`init()` never rejects and never runs twice: concurrent callers share one attempt.\n\n| Situation                                       | Result                                                      |\n| ----------------------------------------------- | ----------------------------------------------------------- |\n| A `datafile` was passed as a bootstrap          | `{ success: true, source: 'bootstrap' }`                    |\n| A cached datafile was still fresh               | `{ success: true, source: 'bootstrap' }`                    |\n| The datafile was fetched                        | `{ success: true, source: 'network' }`                      |\n| The fetch failed and a stale cached copy exists | `{ success: true, degraded: true, source: 'error', error }` |\n| The fetch failed with nothing cached            | `{ success: false, source: 'error', error }`                |\n\n`degraded: true` means one thing only: the SDK is serving a stale cached datafile after a failed refresh. Flags still evaluate, but a change published since then is not visible yet.\n\nReading a flag before `init()` finishes returns your default with `source: 'not_ready'` and warns once per flag key. Adapters await `init()` for you.\n\n---\n\n## 8. Caching\n\nTwo separate ideas, deliberately:\n\n- **Freshness** is the client's `cacheTtlMs` (default 5 minutes). Past it, the client revalidates over the network, and if that fails it serves the stale copy with `degraded: true`.\n- **Retention** is how long an adapter's store keeps an entry at all. It is garbage collection, nothing more: Cloudflare KV defaults to 24 hours (`kvRetentionSeconds`), the Vercel and Bun in-process maps default to 1 hour (`memoryRetentionMs`).\n\nKeeping them apart is what makes the degraded path reachable. A store that hard-expired at the freshness window would leave nothing to fall back on when the CDN is unreachable.\n\nVercel Edge Config is read-only at runtime, so `datafileSet` warms the in-process map only. Populate the Edge Config key `avsb_df_<sdkKey>` from CI if you want a shared cache.\n\n---\n\n## 9. Logging and error handling\n\n```ts\nlogger?: Logger\nlogLevel?: 'debug' | 'info' | 'warn' | 'error' | 'silent'\nonError?: (error: Error, source: 'init' | 'cache' | 'events' | 'heartbeat') => void\n```\n\nUnlike the browser SDK, the edge default is never silent. A browser console belongs to your end user; a Worker's log stream belongs to you, and there is no build step that flips a production define for a Worker. The default is console at `warn`, or `debug` when `globalThis.__AVSB_DEV__` is true or `NODE_ENV` is a development value.\n\nConfiguration-level messages are deduplicated for the life of the isolate, so a Worker serving ten thousand requests with a bad key logs it once, not ten thousand times.\n\nSet `logLevel: 'silent'` for no output at all.\n\n```ts\nimport { createCloudflareHandler } from '@avsbhq/edge/cloudflare';\n\nconst avsb = createCloudflareHandler({\n  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',\n  clientOptions: {\n    logLevel: 'debug',\n    onError: (error, source) => {\n      if (source === 'init') console.error('avsb init failed', error.message);\n    },\n  },\n  handler: async (req, client) => Response.json({ ok: client.isReady() }),\n});\n```\n\nThe SDK never throws from a read, a track, or a flush.\n\n---\n\n## 10. Client options\n\nEvery field, as declared in `src/types.ts`:\n\n```ts\ninterface EdgeClientOptions {\n  sdkKey: string;\n  context?: EvalContext;\n  datafile?: FlagDatafile;\n  fetchDatafile?: (sdkKey: string) => Promise<FlagDatafile | null>;\n  cdnHost?: string;\n  storage?: UnifiedStorageAdapter;\n  cacheTtlMs?: number;\n  timeoutMs?: number;\n  heartbeat?: boolean;\n  waitUntil?: (p: Promise<unknown>) => void;\n  pageUrl?: string;\n  logger?: Logger;\n  logLevel?: EdgeLogLevel;\n  onError?: (error: Error, source: EdgeErrorSource) => void;\n  maxQueueSize?: number;\n  eventRetryAttempts?: number;\n}\n```\n\nAdapters own `sdkKey`, `storage`, `waitUntil`, `context`, and `pageUrl`, because all five are derived from the request and the bindings you gave them. Everything else is passed through `clientOptions`.\n\nDefaults: `cacheTtlMs` 300000, `timeoutMs` 3000, `heartbeat` true, `maxQueueSize` 500, `eventRetryAttempts` 1.\n\n---\n\n## 11. Using the client directly\n\nIf you own the request lifecycle, skip the adapters:\n\n```ts\nimport { AvsbEdgeClient, readVisitorId } from '@avsbhq/edge';\n\nexport async function handle(req: Request): Promise<Response> {\n  const client = new AvsbEdgeClient({\n    sdkKey: process.env.AVSB_SDK_KEY ?? '',\n    context: { kind: 'user', key: readVisitorId(req.headers) ?? '' },\n    pageUrl: req.url,\n  });\n\n  await client.init();\n\n  const flag = client.getBoolFlag('new-checkout', false);\n  const response = Response.json({ enabled: flag.isEnabled() });\n\n  await client.flushEvents();\n  return response;\n}\n```\n\n`init()` resolves `{ success: false }` when there was nothing to serve. The SDK has already logged why, so branching on it is optional: flags return the defaults you passed either way.\n\nThere is nothing to close: an edge client lives for one request and has no timers, no sockets, and no global state.\n\n---\n\n## 12. Testing\n\nPass a datafile as a bootstrap and no network call happens:\n\n```ts\nimport { AvsbEdgeClient } from '@avsbhq/edge';\nimport type { FlagDatafile } from '@avsbhq/edge';\n\nconst datafile: FlagDatafile = {\n  version: 2,\n  projectType: 'FEATURE_FLAG',\n  sdkKey: 'sdk_test_aaaaaaaaaaaaaaaaaaaaaaaa',\n  environmentKey: 'test',\n  publishedAt: '2026-01-01T00:00:00.000Z',\n  collectEndpoint: 'https://ingest.avsb.cloud',\n  flags: [\n    {\n      id: 'flag_1',\n      key: 'new-checkout',\n      type: 'boolean',\n      enabled: true,\n      defaultVariationId: 'var_off',\n      variations: [\n        { id: 'var_on', key: 'on', value: true },\n        { id: 'var_off', key: 'off', value: false },\n      ],\n      overrides: [],\n      rules: [\n        {\n          id: 'rule_1',\n          type: 'targeted_delivery',\n          enabled: true,\n          audienceIds: [],\n          hashAttribute: 'userId',\n          trafficAllocation: 1,\n          variations: [{ variationId: 'var_on', percentage: 1 }],\n        },\n      ],\n    },\n  ],\n  audiences: [],\n};\n\nconst client = new AvsbEdgeClient({\n  sdkKey: 'sdk_test_aaaaaaaaaaaaaaaaaaaaaaaa',\n  datafile,\n  context: { kind: 'user', key: 'user_42' },\n  logLevel: 'silent',\n});\nawait client.init();\n\nclient.getBoolFlag('new-checkout', false).value; // true\n```\n\nSet `logLevel: 'silent'` in tests so SDK diagnostics stay out of your output.\n\n---\n\n## 13. Breaking changes in 1.x\n\n**B7. The event wire format changed.** Older builds posted `{sdkKey, events: [{eventKey, context, value, properties, timestamp}]}` to the bare `collectEndpoint` with no `/v1/collect/*` path and no event ids. Nothing on the platform accepted that shape, so no edge event was ever recorded. Events now go to `/v1/collect/batch` and `/v1/collect/metric-batch` in the format every other A vs B SDK uses. `buildCollectPayload` and `CollectEventPayload` are deleted; use `client.track` and `client.flushEvents`.\n\nAlso changed in the same release, all of which were broken rather than merely different:\n\n- The datafile URL is `{cdnHost}/{sdkKey}/datafile.json`. It used to be `{cdnHost}/datafiles/{sdkKey}.json`, which 404s.\n- Evaluation fires exposures. It used to pass no exposure hook, so edge experiments produced no data.\n- `contextFrom` is called, and is optional because each adapter has a real default. It used to be declared and never called.\n- `init()` returns an `InitResult` instead of `void`.\n- A read before `init()` reports `not_ready` instead of `not_found`.\n- The Fastly handler takes the fetch event rather than the request, so geolocation is reachable.\n- The Lambda@Edge handler may return nothing to continue to the origin, and its responses now carry a body.\n- `createCloudflareHandler` accepts `sdkKey` and `kv` as functions of `env`.\n- Typed getters (`getBoolFlag`, `getStringFlag`, `getNumberFlag`, `getJsonFlag`) exist. The old README documented them; the code did not have them.\n\n- The default logger prints instead of dropping everything.\n\n**B36. `track` writes both numeric columns.** `payload.revenue` now lands in the wire's `revenue` column and `payload.value` in the separate `value` column, matching the browser, node, Python, Ruby and PHP SDKs and the locked wire fixture. This client used to send `payload.value` into `revenue` and never set `value`, so a quantity was stored as money and an edge-tracked average-value metric read empty. Rename the field to `revenue` wherever you were tracking money.\n\n---\n\n## 14. Migration\n\n### From LaunchDarkly Cloudflare\n\n| LaunchDarkly                                  | `@avsbhq/edge/cloudflare`                               |\n| --------------------------------------------- | ------------------------------------------------------- |\n| `init(sdkKey, { kvNamespace })`               | `createCloudflareHandler({ sdkKey, kv, handler })`      |\n| `client.variation('key', ctx, default)`       | `client.getBoolFlag('key', default).value`              |\n| `client.variationDetail('key', ctx, default)` | `client.getFlag('key', default)`                        |\n| `client.flush()`                              | handled by the adapter, or `await client.flushEvents()` |\n\n### From Statsig Edge\n\n| Statsig                          | `@avsbhq/edge`                                          |\n| -------------------------------- | ------------------------------------------------------- |\n| `StatsigServer.initialize(key)`  | `createCloudflareHandler({ sdkKey, handler })`          |\n| `checkGate(user, 'gate')`        | `client.getBoolFlag('gate', false).isEnabled()`         |\n| `logEvent(user, 'event', value)` | `client.track('event', { value })`                      |\n| `flush()`                        | handled by the adapter, or `await client.flushEvents()` |\n\nDifferences worth knowing:\n\n- Every adapter is per request. There is no singleton to initialise and no process-wide state.\n- The datafile is cached in the runtime's own store, not refetched on every request.\n- Multi-context is native: return `{ kind: 'multi', ... }` from `contextFrom`.\n- There is no `setInterval` and no `EventSource`, because edge environments have no persistent event loop.\n","readmeFilename":"README.md"}