> ## Documentation Index
> Fetch the complete documentation index at: https://docs.varianz.io/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK

> Instrument Node.js services in TypeScript or JavaScript with @vpoint decorators or createVPoints: registration, stages, sessions, and the registry connection.

The Node.js application SDK is `@varianz/node` ([install](/installation#typescript)); it works from TypeScript or plain JavaScript. Test-side integrations (`@varianz/vitest`, `@varianz/playwright`) are covered under [Testing](/testing/vitest).

Requires Node.js 22+ and, for the decorator API, TC39 (stage-3) decorators — TypeScript ≥ 5.0 with `experimentalDecorators: false`.

## Two registration styles

| Style      | API                             | Use when                                                 |
| ---------- | ------------------------------- | -------------------------------------------------------- |
| Decorator  | `@vpoint` on a class method     | Class-based service code (Nest, Fastify service classes) |
| Functional | `createVPoints(namespace, api)` | Plain objects with methods, functional style             |

Both produce the same runtime handles and stage API.

### Decorators

```typescript theme={null}
import { vpoint, Kind, getVPointHandle } from '@varianz/node';

class TodoService {
  @vpoint({ name: 'todo/add' })
  addTodo(todo: Todo): void { ... }

  @vpoint()   // name defaults to kebab-case: todo-service/list-todos
  listTodos(): Todo[] { ... }
}

const service = new TodoService();               // decorators fire on construction
const handle = getVPointHandle(service, 'addTodo');
```

TC39 decorators register via `context.addInitializer(...)`, so **registration happens when the class is instantiated**, not at import time. Construct the service before calling `getVPointHandle(...)`.

### `createVPoints`

```typescript theme={null}
import { createVPoints } from '@varianz/node';

const api = {
  greet(name: string) { return `Hello, ${name}`; },
  sum(a: number, b: number) { return a + b; },
};

const { handles, instrumented } = createVPoints('demo/api', api);

instrumented.greet('World');   // routed through stages
```

Patch modes (`opts.patch`):

* `'wrap'` *(default)* — returns a proxy; the original object is untouched. **Calls on the original object are not intercepted** — use the returned `instrumented`, or:
* `'mutate'` — replaces methods on the original object in place (logs a one-time warning).
* `'off'` — registers without intercepting; dispatch manually via `callVPoint()`.

### Other registration paths

* `registerVPoint(vpdBytes, api)` — from an offline-built descriptor; `encodeVpd(spec)` builds one from a `DynamicSpec`.
* `discover('src/services/**/*.service.ts')` — loads matching files and instantiates exported classes so `@vpoint` initializers fire. Runs in-process, so `.ts` globs 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`](/guides/build-integration#typescript) 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:

```typescript theme={null}
@vpoint({
  name: 'pricing/calculate',
  params: [{ name: 'input', kind: structKind('pricing/PricingInput') }],
  returnKind: structKind('pricing/PricingResult'),
})
calculate(input: PricingInput): PricingResult { ... }
```

## Local stages

Uniquely, the Node SDK can attach stages in-process without the registry — JS functions, CEL, or debug logging — at any anchor:

```typescript theme={null}
import { attachStage, attachCelStage, attachDebugStage, setStageEnabled, removeStage, Anchor } from '@varianz/node';

const stage = attachStage(handle, Anchor.Pre, 'log-calls', (ctx) => {
  console.log('args:', ctx.args);        // return a value to short-circuit
});
setStageEnabled(stage, false);
removeStage(stage);

attachCelStage(handle, Anchor.Validate, 'check-age', 'args.arg0 >= 18');
attachDebugStage(handle, Anchor.Pre, { name: 'dbg', logArgs: true, logResult: true });
```

Stage handles are async-disposable on Node 23+ (`using stage = attachStage(...)`); on Node 22 call `stage.remove()` explicitly. `attachCelStage` and `attachDebugStage` also accept a VPoint name instead of a handle.

## Sessions

The SDK holds the session in `AsyncLocalStorage`, exposed as `VarianzContext`:

```typescript theme={null}
import { VarianzContext } from '@varianz/node';

app.use(VarianzContext.expressMiddleware());          // extract from x-varianz-id

await VarianzContext.session(sessionHex, async () => { ... });   // explicit scope
const ctx = VarianzContext.current();                 // { sessionId?, traceId?, ... }
fetch(url, { headers: VarianzContext.getHeaders() }); // outbound injection
```

A VPoint method that receives the session ID as a parameter can declare it with `varianzId('sessionId')` in its params, and the SDK enters the scope automatically. Framework-specific wiring (Fastify, Koa, Hono, NestJS, gRPC): [Propagate sessions](/guides/propagate-sessions#typescript).

## Registry connection

```typescript theme={null}
import { initRegistry, onRegistryUpdate } from '@varianz/node';

initRegistry({
  endpoint: 'http://localhost:50051',
  applicationName: 'my-service',
  region: 'us-east-1',          // optional environment tags
  cluster: 'production',
  tags: { version: '1.0.0' },
});

onRegistryUpdate(
  (event) => console.log(`${event.action}: ${event.stageName}`),
  (err) => console.error('registry error:', err),
);
```

`initRegistry` is process-global and throws on a second call — call it exactly once at startup (the test fixtures guard this for you). The environment tags determine which [environment-scoped stages](/reference/naming-and-routing#environment-filters) 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(...)`.
* **`initRegistry` throws "already initialised"** — it's process-global. In Vitest, set `fileParallelism: false`.
* **Calls aren't intercepted** — you're calling the original object with the default `wrap` patch mode. Call through `instrumented` or use `patch: 'mutate'`.
* **`varianz-scan` finds no VPoints** — the scanner pre-filters files containing `vpoint`/`createVPoints` call sites; importing types alone registers nothing.
