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

# Node

> Server-side assignment and tracking for Node, Bun, Deno, and edge runtimes.

`@trevosdk/node` runs experiments and records conversions from your backend. It uses
web-standard APIs only — `fetch`, `AbortSignal`, timers — so it runs on Node 18+, Bun, Deno,
and edge runtimes.

```bash theme={null}
npm install @trevosdk/node
```

## Why use it

**Conversions that cannot be blocked.** A `subscription_started` fired from a Stripe webhook
is never lost to an ad blocker, a closed tab, or a failed beacon. This improves the numbers
for your *browser* experiments too, which is the most common reason to adopt it before
running a single backend experiment.

**Experiments only a backend can run** — pricing and packaging, search ranking, prompt or
model choices, lifecycle email content. The variant is a code path, not a UI element.

**Properties the browser shouldn't be trusted with** — plan, seat count, MRR.

## Create a client

You need a **server** key: `tsk_secret_…`, from Settings → SDK keys with platform `Server`.
It is shown once. Never put it in client code — secret keys sent with an `Origin` header are
rejected.

```ts theme={null}
import { createClient } from '@trevosdk/node';

const secretKey = process.env.TREVO_SECRET_KEY;
if (!secretKey) throw new Error('Missing TREVO_SECRET_KEY');

export const trevo = createClient({ secretKey });
```

Create the client at module scope — one per process, reused across requests — then
`await trevo.ready()` inside your request or startup lifecycle. Starting network work inside
the lifecycle also works in edge runtimes that forbid it during module evaluation.

## Identity is passed per call

A server holds no ambient user state, so every call takes the identity explicitly:

```ts theme={null}
await trevo.ready();
const userId = req.user?.id;
const anonymousId = req.cookies.trevo_id;
const identity = {
  ...(userId ? { userId } : {}),
  ...(anonymousId ? { anonymousId } : {}),
};
const variant = trevo.getVariant('pricing-algo-v2', identity);

trevo.track('subscription_started', identity, { plan: 'pro' });
```

**Send both ids whenever you have them.** Trevo links an anonymous journey to a user the
first time it sees an event carrying both. A backend that only ever sends `userId` never
creates that link, and the user's pre-login activity is never attached to their conversion.

**Bucket with the identity the browser is using at that moment** — the user id when
identified, otherwise the `trevo_id` cookie. Anything else assigns the same person different
variants on the client and the server.

## Linking identities without a cookie

Webhooks and cron jobs have no cookie to read. State the link directly:

```ts theme={null}
await trevo.alias(anonymousId, userId);
```

Idempotent and safe to retry. An `anonymousId` belongs to one user — pointing it at a
different user changes nothing and **throws** `TrevoSDKError` (HTTP 409). Retrying can't
succeed — catch it rather than letting your queue replay.

## Idempotency

Payment providers replay webhooks, and a conversion counted twice inflates whichever variant
that user was in. Pass a stable `insertId` derived from the thing that happened:

```ts theme={null}
trevo.track(
  'subscription_started',
  { userId },
  { plan: 'pro' },
  { insertId: subscription.id },
);
```

Trevo records that event once regardless of how many deliveries arrive.

## Serverless

Timers do not survive between invocations, so flush explicitly before returning:

```ts theme={null}
export async function handler(event) {
  await trevo.ready();
  trevo.track('order_placed', { userId: event.userId }, { value: 42 });
  await trevo.flush();          // rejects if delivery failed, so you can react
}
```

For long-running processes, call `shutdown()` before exit — it stops timers and flushes once.

Cloudflare Workers expose secrets through the request's `env` binding rather than
`process.env`. Cache construction at module scope, but read the binding and start network work
inside `fetch`:

```ts theme={null}
import { createClient } from '@trevosdk/node';

let client: ReturnType<typeof createClient> | undefined;

export default {
  async fetch(_request, env, ctx) {
    if (!env.TREVO_SECRET_KEY) throw new Error('Missing TREVO_SECRET_KEY binding');
    client ??= createClient({ secretKey: env.TREVO_SECRET_KEY });
    await client.ready();
    // Resolve and track with the userId from existing auth, or the same
    // trevo_id cookie the browser uses. Never pass an empty identity.
    ctx.waitUntil(client.flush());
    return new Response('ok');
  },
};
```

## Local development

Pin variants without touching Trevo:

```bash theme={null}
TREVO_FORCE_VARIANTS=checkout-cta:treatment,pricing-v2:control
```

Or in code:

```ts theme={null}
createClient({ secretKey, forceVariants: { 'checkout-cta': 'treatment' } });
```

Forced reads never record an exposure, and while a force is pinned every event the client
sends is marked `preview`, which every experiment read excludes — so your local testing never
pollutes results, conversions included. A force naming a variant the experiment
doesn't have is reported through `onError` rather than ignored quietly.

To render for a browser previewing with `?trevo_force=`, pass that request's force — it wins
over the process-wide one — and mark the events that request produces:

```ts theme={null}
import { parseForceVariants } from '@trevosdk/node';

const forceVariants = parseForceVariants(url.searchParams.get('trevo_force'));
const variant = trevo.getVariant('checkout-cta', identity, { forceVariants });
trevo.track('checkout_started', identity, props, {
  forcedPreview: Object.keys(forceVariants).length > 0,
});
```

## Options

| Option | Default | Notes |
| - | - | - |
| `secretKey` | — | Required. `tsk_secret_…` |
| `pollIntervalMs` | `60000` | Config refresh cadence; minimum `10000` |
| `flushIntervalMs` | `5000` | Background flush cadence; minimum `500`. Serverless should `await flush()` instead |
| `maxBatchSize` | `50` | Requests capped at 500 by the server |
| `configUrl` | — | Endpoint override for proxied installs |
| `ingestionUrl` | — | Endpoint override for proxied installs |
| `aliasUrl` | — | Endpoint override for proxied installs |
| `bootstrapConfig` | — | Assign against this before the first fetch resolves, so a cold start never serves control |
| `forceVariants` | — | Pin variants locally; also read from `TREVO_FORCE_VARIANTS` |
| `fetch` | global | Override for tests or runtimes without a global fetch |
| `onError` | `console.error` | Background failures that have nowhere else to surface |

## API summary

| Method | Purpose |
| - | - |
| `ready()` | Resolves once initial config is loaded, or the first fetch failed |
| `getVariant(key, identity, options?)` | Deterministic assignment |
| `track(name, identity, properties?, options?)` | Queue a conversion |
| `alias(anonymousId, userId)` | Link an anonymous journey to a user |
| `flush()` | Send everything queued; rejects on failure |
| `shutdown()` | Stop timers and flush once |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.