@varianz/node (install); it works from TypeScript or plain JavaScript. Test-side integrations (@varianz/vitest, @varianz/playwright) are covered under Testing.
Requires Node.js 22+ and, for the decorator API, TC39 (stage-3) decorators — TypeScript ≥ 5.0 with experimentalDecorators: false.
Two registration styles
Both produce the same runtime handles and stage API.
Decorators
context.addInitializer(...), so registration happens when the class is instantiated, not at import time. Construct the service before calling getVPointHandle(...).
createVPoints
opts.patch):
'wrap'(default) — returns a proxy; the original object is untouched. Calls on the original object are not intercepted — use the returnedinstrumented, or:'mutate'— replaces methods on the original object in place (logs a one-time warning).'off'— registers without intercepting; dispatch manually viacallVPoint().
Other registration paths
registerVPoint(vpdBytes, api)— from an offline-built descriptor;encodeVpd(spec)builds one from aDynamicSpec.discover('src/services/**/*.service.ts')— loads matching files and instantiates exported classes so@vpointinitializers fire. Runs in-process, so.tsglobs need a TS loader (tsx,ts-node, or--experimental-strip-types).
Schemas
Schemas come from four sources, in fixed precedence: explicit registration options → the@varianz/scanner build-time manifest → first successful invocation → auto-derived Unknown.
Run the scanner and your TS types become the schema source — decorators like @struct/@field are then overrides for special cases (canonical names, Kind.Int32 instead of the Float64 default for number, field access modes). Values typed Unknown still support CEL field access at runtime; only CEL struct-literal construction needs a typed schema.
Kinds cover the numeric range (Int8–Int128, UInt8–UInt128, Float32/64), Bool, Utf8, Bytes, Struct, and Unknown. Named struct kinds link a parameter to a @struct declaration:
Local stages
The Node SDK can attach stages in-process without a coordinator — JS/TS functions, CEL, or debug logging — at any anchor. Every attach function takes the same target: a VPoint handle, aVPointRef from createVPoints, or a lookup name.
ctx.argsholds the call’s arguments,ctx.selfthe receiver,ctx.vpLookupNamethe VPoint name. TypeScript erases parameter names, so the keys arearg0,arg1, … unless the scanner has run or the VPoint declaresparamsexplicitly.- Return
CONTINUEto continue the pipeline. Return anything else — includingundefined— to override the result. - Overrides are converted against the VPoint’s declared return kind; a mistyped one fails the call rather than handing the caller the wrong type.
- Stage functions must be synchronous. Returning a
Promisethrows instead of being silently un-awaited. - Anchors are exactly
Anchor.Validate | Pre | Mid | Post | Audit; the original function runs after the last one.
stage.remove() explicitly:
Typed helpers
varianz-scan emits stages.gen.js and stages.gen.d.ts into its output directory (node_modules/.varianz by default), with one helper per scanned VPoint. The helper gives ctx.args the real parameter names and checks the override’s type:
VPointRef for a different one throws at attach time instead of attaching a mistyped stage. If importing out of node_modules/.varianz is awkward, point the scanner’s outputDir at a source path or add a tsconfig paths alias.
Local stages apply to every call in the process, not only calls carrying a session ID. Use them for behavior owned by the process under test; use CEL stages through Vitest or Playwright when you need session isolation, cross-service delivery, or samples.
Sessions
The SDK holds the session inAsyncLocalStorage, exposed as VarianzContext:
varianzId('sessionId') in its params, and the SDK enters the scope automatically. Framework-specific wiring (Fastify, Koa, Hono, NestJS, gRPC): Propagate sessions.
Coordinator connection
initCoordinator is process-global: a second call with the same configuration returns false and is a no-op; a second call with a different configuration throws coordinator already initialized with a different configuration. Call it once at startup (the test fixtures guard this for you).
Omitting endpoint makes the SDK resolve one from VARIANZ_COORDINATOR_ENDPOINT, then varianz.toml, and raise if neither supplies one — it does not fall back to a local-only runtime. Supplying endpoint does not make it authoritative: VARIANZ_COORDINATOR_ENDPOINT outranks it. The test-side clients additionally accept the deprecated VARIANZ_TEST_ENDPOINT / VARIANZ_COORDINATOR_ADDR / VARIANZ_ENDPOINT spellings, with a warning.
The environment tags determine which environment-scoped stages reach this instance.
For diagnostics: initTracing('info') or initTracing('debug', { json: true }).
Troubleshooting
- Decorators don’t register — decoration runs on instantiation. Construct the service before
getVPointHandle(...). initCoordinatorthrows “already initialized with a different configuration” — it’s process-global and the second call disagreed with the first. In Vitest, setfileParallelism: false. A second call with an identical configuration is harmless and returnsfalse.- Calls aren’t intercepted — you’re calling the original object with the default
wrappatch mode. Call throughinstrumentedor usepatch: 'mutate'. varianz-scanfinds no VPoints — the scanner pre-filters files containingvpoint/createVPointscall sites; importing types alone registers nothing.
