Skip to main content
@traffical/node is the server-side SDK for Node.js and Bun. It fetches the config bundle on startup, resolves parameters locally per request, and ships events to Traffical in the background.

Installation

npm install @traffical/node

Initialization

import { createTrafficalClient } from "@traffical/node";

const traffical = await createTrafficalClient({
  orgId: "org_acme",
  projectId: "proj_marketplace",
  env: "production",
  apiKey: process.env.TRAFFICAL_API_KEY!,
});
Use createTrafficalClient (async) when you can await at startup — it waits for the first bundle fetch. If you can’t await (e.g. in module top-level code where top-level await isn’t available), use createTrafficalClientSync and call await traffical.waitUntilReady() before the first resolution.

Options

OptionTypeDefaultDescription
orgIdstringrequiredOrganization ID
projectIdstringrequiredProject ID
envstringrequiredEnvironment name ("production", "staging", etc.)
apiKeystringrequiredSDK key (traffical_sk_...)
baseUrlstringhttps://sdk.traffical.ioSDK API base URL
refreshIntervalMsnumber60000How often to re-fetch the bundle
localConfigConfigBundleEmbed a bundle for instant cold-start
eventBatchSizenumber10Events per batch before flushing
eventFlushIntervalMsnumber30000Max time before flushing pending events
trackDecisionsbooleantrueEmit decision events alongside exposures
attributionMode"cumulative" | "decision""cumulative"Track-event attribution strategy

Resolving parameters

const params = traffical.getParams({
  context: { userId: "user_789", locale: "en-US", plan: "pro" },
  defaults: {
    "checkout.button.color": "#1E6EFB",
    "checkout.headline": "Complete your order",
    "checkout.show_trust_badges": false,
    "pricing.discount_pct": 0,
  },
});

params["checkout.button.color"];     // "#22C55E" if assigned to treatment
params["checkout.show_trust_badges"]; // true if policy overrides it
params["pricing.discount_pct"];       // 0 (no policy → default)

Context

The context object is used for:
  • Bucketing — the unit key (typically userId, as configured on the project) drives which allocation the user gets.
  • Targeting — every field is available for policy condition evaluation. A policy with the condition plan in ["pro", "enterprise"] only applies when context.plan is one of those values.

Defaults

Always pass defaults for every parameter you read. They’re the fallback when no policy matches, when the bundle isn’t loaded yet, or when Traffical is unreachable — and they make your code’s intent explicit even when there’s no active experiment.

Decisions vs getParams

getParams returns just the resolved values. decide returns a full decision record with decisionId and the layer-by-layer assignments — useful when you want to pass the decision ID downstream:
const decision = traffical.decide({
  context: { userId: req.user.id },
  defaults: { "recommendations.algorithm": "collaborative" },
});

const recs = await getRecommendations(req.user.id, decision.assignments["recommendations.algorithm"]);

res.json({
  recommendations: recs,
  meta: { decisionId: decision.decisionId },   // frontend can attribute clicks back to this decision
});
See decisions & attribution for when this matters.

Tracking events

// Conversion with a value
traffical.track("purchase", {
  unitKey: "user_789",
  properties: { order_total: 49.99, currency: "USD" },
});

// Action without a value
traffical.track("add_to_cart", { unitKey: "user_789" });

// Cross-process — attribute to a specific decision
traffical.track("page_view", {
  unitKey: "user_789",
  decisionId: "dec_abc123",
  properties: { path: "/checkout" },
});

Track options

FieldTypeRequiredDescription
unitKeystringyesThe user ID (or whatever the project’s unit key is)
propertiesobjectnoEvent payload — anything relevant
decisionIdstringnoTie this event to a specific decision (cross-process)

Express middleware pattern

import express from "express";
import { createTrafficalClient } from "@traffical/node";

const app = express();

const traffical = await createTrafficalClient({
  orgId: "org_acme",
  projectId: "proj_marketplace",
  env: "production",
  apiKey: process.env.TRAFFICAL_API_KEY!,
});

app.use((req, res, next) => {
  const userId = req.session?.userId ?? req.cookies?.anonymousId;
  if (userId) {
    req.traffical = traffical.getParams({
      context: { userId, locale: req.headers["accept-language"] },
      defaults: {
        "checkout.button.color": "#1E6EFB",
        "checkout.show_trust_badges": false,
      },
    });
  }
  next();
});

app.get("/checkout", (req, res) => {
  res.render("checkout", {
    buttonColor: req.traffical["checkout.button.color"],
    showBadges: req.traffical["checkout.show_trust_badges"],
  });
});

Type-safe events

Generate TypeScript types from your event definitions to catch invalid event names and properties at compile time:
bunx @traffical/cli generate-types
import type { TrafficalEventProperties } from "./traffical.generated";

type TypedTrack = <E extends keyof TrafficalEventProperties>(
  event: E,
  options: { unitKey: string; properties?: TrafficalEventProperties[E]; decisionId?: string }
) => void;

const track = traffical.track.bind(traffical) as TypedTrack;

track("purchase", {
  unitKey: "user_789",
  properties: { order_total: 99.99, payment_method: "visa" },     // ✅
});

track("purchase", {
  unitKey: "user_789",
  properties: { order_total: 99.99, unknown_field: true },        // ❌ compile error
});

Error handling

The SDK never throws during resolution. If the bundle isn’t loaded yet — or if it never loaded successfully — getParams returns your defaults. Initialization errors surface as a rejected promise:
const traffical = await createTrafficalClient({
  orgId, projectId, env, apiKey,
}).catch((err) => {
  console.error("Failed to initialize Traffical:", err);
  return null;
});
You can keep a null client around and guard your calls — but more often it’s cleaner to use createTrafficalClientSync, which returns a usable client immediately (resolution falls back to defaults until the bundle arrives) and never rejects at construction.

Shutdown

Flush pending events before your process exits:
await traffical.close();
This sends any buffered events and stops the background refresh.

Next steps

A/B testing

Run a static experiment.

Canonical experiments

Patterns for backend, batch, and cross-surface tests.

CLI

Manage parameters as code.

API reference

The endpoints the SDK calls.