Typed contexts

defineContextSchema() creates a compile-time contract between the attributes your application passes to the SDK and the attribute keys declared in your flag targeting rules (the conditions that decide what each visitor sees). Mismatches, like a typo in a field name, a type mismatch, or a removed attribute still referenced in a rule, become TypeScript errors before they ship.

Coming from the getting-started journey and just want the types? Run npx @avsbhq/cli codegen --project PRJ-42 --output ./src/generated/flags.ts (inside a folder created by avsb project pull or avsb clone, --project can be left out).

What it is

Without typed contexts, the evaluation context is a plain object with an open [attribute: string]: unknown index signature, so any key is allowed. You can write usrId: 'u_1' instead of userId: 'u_1' and nothing warns you. The SDK evaluates silently: your visitor falls through to the default variation, meaning one specific version being tested, like the control, because the targeting condition never fires.

defineContextSchema() replaces the open bag with a Zod-validated, fully typed builder. The schema lives next to your SDK client setup, so TypeScript enforces it everywhere a context gets built: in components, in middleware, and in tests.

TypeScript
import { z } from 'zod'import { defineContextSchema } from '@avsbhq/utils'// Declare the shape onceexport const Ctx = defineContextSchema({  user: z.object({    id: z.string(),    plan: z.enum(['free', 'pro', 'enterprise']),    country: z.string().optional(),  }),  organization: z.object({    id: z.string(),    tier: z.number().int().min(1).max(5),  }),})// Build contexts with full type-checkingconst context = Ctx.builder()  .setUser('u_123', { id: 'u_123', plan: 'pro', country: 'US' })  .setOrganization('o_42', { id: 'o_42', tier: 3 })  .build()
TypeScript21 lines

A typo no longer compiles. This is the failure the schema exists to produce, so it is shown here as the compiler output rather than as code you could run:

Plain text
Ctx.builder().setUser('u_123', { usrId: 'u_123', plan: 'pro' })//                               ^^^^^ TS2353: Object literal may only specify//                               known properties, and 'usrId' does not exist//                               in type Partial<{ id: string; plan: ... }>
Plain text4 lines

When to use it

Typed contexts have the highest value when:

  • Your flag targeting rules use many custom attributes, like plan tiers, roles, or feature flags on the user record (settings that turn a feature on or off without a new deploy). You want TypeScript to check that these attribute keys match between your rules and your SDK calls.
  • You use the CLI's avsb codegen command, which emits a ContextSchema interface matching your platform's declared registeredAttributes. This creates an end-to-end type chain: platform schema → generated TypeScript → SDK call sites.
  • Your codebase has multiple teams passing context objects and you want a single source of truth for the attribute shape.

When not to use it

Skip typed contexts when:

  • Your codebase is plain JavaScript, not TypeScript. The main benefit, a typo that cannot compile, is a TypeScript-only guarantee. A JavaScript file can still call Ctx.validate(ctx) for a runtime check, but it never gets the "the typo cannot ship" protection.
  • You only have one or two attributes. Wrapping a two-field context in a schema, a builder, and a generated file adds more setup than it saves. Build the plain context object shown in SDK Installation instead.
  • Your attributes are still changing every day. Early on, keeping the schema, the generated file, and every call site in sync during heavy churn costs more time than the typos it would catch. Add typed contexts once your targeting attributes settle down.

How it works

defineContextSchema returns an object with two members:

  • builder(): returns a typed ContextBuilder whose setter methods accept only the declared attribute shape for the given kind. The returned EvalContext from build() is a plain object compatible with every SDK API.
  • validate(ctx): runs the Zod schema against any EvalContext and returns { success: true, data } on success or { success: false, error: { issues } } on failure. This is a small result shape of its own, not Zod's SafeParseReturnType (Zod's version types the error as a ZodError, not a plain issues array). Useful in middleware to verify incoming contexts before forwarding to the evaluator.
TypeScript
import type { EvalContext } from '@avsbhq/node'// Your app, your request type, and your own two helpers.interface AvsbRequest {  avsbContext?: EvalContext}declare const app: {  use(handler: (req: AvsbRequest, res: unknown, next: () => void) => void): void}declare function buildContextFromRequest(req: AvsbRequest): EvalContextdeclare const logger: { warn(message: string, meta: Record<string, unknown>): void }// Runtime validation in Express middlewareapp.use((req, res, next) => {  const raw = buildContextFromRequest(req)  const result = Ctx.validate(raw)  if (!result.success) {    logger.warn('invalid avsb context', { errors: result.error.issues })    // Fall through with a safe anonymous context    req.avsbContext = { kind: 'user', key: 'anonymous' }  } else {    req.avsbContext = result.data  }  next()})
TypeScript26 lines

CLI codegen integration

The avsb codegen command reads your platform project's declared context kinds and attribute definitions and emits a typed schema file:

Shell
npx @avsbhq/cli codegen --output ./src/generated/flags.ts --project 42
Shell1 line

The generated file exports three types:

TypeScript
// generated/flags.ts (example output)export type FlagKey =  | 'checkout_v2'  | 'homepage_hero'  | 'pricing_experiment'export interface FlagValues {  'checkout_v2': boolean  'homepage_hero': 'control' | 'variant-a' | 'variant-b'  // codegen reads every variation's actual value and infers a real shape,  // marking a key optional (`?`) when one variation is missing it. Only past  // a size or nesting limit does it give up and emit `unknown`.  'pricing_experiment': { discountPercent: number; freeShipping?: boolean }}export interface ContextSchema {  'organization': { key: string; 'tier': number }  'user': { key: string; 'plan': string }}
TypeScript19 lines

Every key here, including the context kind itself, is quoted in the real output, and an attribute's type is always one of string, number, boolean or unknown (never a literal union like 'free' | 'pro'): the platform stores a coarse type per attribute, not its possible values.

Import ContextSchema into your defineContextSchema call to keep your local schema in sync with the platform:

TypeScript
import { z } from 'zod'import { defineContextSchema } from '@avsbhq/utils'import type { ContextSchema } from './generated/flags'// Zod schema derived from the generated interfaceexport const Ctx = defineContextSchema({  user: z.object({    key: z.string(),    plan: z.enum(['free', 'pro', 'enterprise']),  } satisfies Record<keyof ContextSchema['user'], z.ZodTypeAny>),  organization: z.object({    key: z.string(),    tier: z.number(),  } satisfies Record<keyof ContextSchema['organization'], z.ZodTypeAny>),})
TypeScript15 lines
Tip

Add avsb codegen to your postinstall or pre-build step so the generated file is always fresh when you install dependencies or deploy.

Keeping the generated file honest

There is no avsb lint. Re-run avsb codegen and let your own type-check catch drift. If a targeting attribute disappears from the platform, it stops existing in ContextSchema. The satisfies clause above then turns that gap into a compile error in your code, not a surprise at runtime.

Was this helpful?