# The JS SDK.

`@dif.sh/sdk` is the runtime. Pure TypeScript, zero dependencies. It handles experiment assignment, exposure delivery, and, when you're connected to dif.sh Cloud, metric tracking through the same import.

You almost never call the SDK directly for experiments. `dif build` emits `dif/generated/client.ts`, which registers every active experiment with the SDK as a side effect of importing it. Your code calls `dif("id", branches)` at render sites; the SDK looks up the spec, evaluates the audience, buckets the user, fires one exposure event, and returns the branch's value through a thunk.

Two methods sit alongside that callable: `dif.init(...)` configures the SDK once at boot, and `dif.track(...)` fires a metric event to dif.sh Cloud. Tracking only does anything with a Cloud project + publishable key; without one, calls drop silently.

## Install

```
$npm install @dif.sh/sdk
```

Import the generated client once, at app boot, so its registrations populate the runtime registry:

```
// somewhere in your app's entry point
import "./dif/generated/client";
```

Without this import the SDK has no registered experiments: an unknown id safely returns the first branch, but doesn't bucket or emit an exposure. The framework adapters and the server entrypoint ship alongside:

```
$npm install @dif.sh/react     # <DifProvider> + useDif()
$npm install @dif.sh/svelte    # difLoad + experiment() store
# server-side tracking is a subpath of @dif.sh/sdk — no extra install
import { DifServer } from "@dif.sh/sdk/server";
```

`@dif.sh/sdk` is a peer dependency of both adapters. Install it explicitly. The SDK and adapters need Node 20.6+.

## dif.init()

Configure once, at boot. The SDK needs to know how to look up the current user, what audience attributes they carry, where to send exposure events, and, for Cloud, which project to talk to. The `attributes()` helper comes from the generated audience module; values you pass it override the resolver-produced ones.

```
import { dif } from "@dif.sh/sdk";
import { attributes } from "./dif/generated/audiences";

dif.init({
  project: "acme-shop",                          // cloud project slug
  publishableKey: "dif_pk_live_…",             // browser-safe
  userId: () => currentUser?.id ?? null,
  attributes: () => attributes({
    plan: currentUser?.plan ?? null,
  }),
});
```

| option | default | notes |
| --- | --- | --- |
| `project` Cloud | `—` | The Cloud project slug. Carried by configuration; required to attribute Cloud events. |
| `publishableKey` Cloud | `—` | A `dif_pk_…` token. Browser-safe; embed it in your bundle. Required to ship events to Cloud from a browser. |
| `apiUrl` Cloud | `https://cloud.dif.sh` | Cloud or self-hosted API base URL. |
| `userId` | `() => null` | Resolves the current user id at assignment time. A null user gets the first branch and no exposure. |
| `attributes` | `() => ({})` | Resolves the audience attribute bag. Usually the generated `attributes()` helper. |
| `sink` | cloud sink, else none | Exposure sink or array of sinks. Defaults to the Cloud sink when a publishable key is set. Pass `[]` to disable exposure delivery. |
| `enabled` | `true` | Global assignment + tracking kill switch. `false` short-circuits every `dif()` to control and drops every `track()`. |
| `overrides` | `{}` | Initial QA experiment-to-variant forces. |

> // note `dif.init` configures a module-level singleton; calling it again replaces the previous state. `dif.configure(...)` remains a deprecated alias.

## The dif() call

One call per render site. The experiment id is a string literal; `dif build` validates that every id in source maps to an active `.md` file.

```
import { dif } from "@dif.sh/sdk";

const checkoutCta = dif("checkout-cta-v2", {
  control: () => "Place order",
  variant_a: () => "Get it today",
});

// at render time
button.textContent = checkoutCta();
```

`dif(...)` returns a thunk. Invoking it at the render site is what triggers resolution and exposure firing, so resolution happens at every render, not at module-load time, which matters when the user id changes mid-session. Branches can return any value (a string, a style object, a component). Calling `dif()` with an empty branches object throws.

### Resolution order

One call resolves in order:

1.  **QA force:** a valid force wins and emits no exposure.
2.  **Unknown id:** the registry has no spec. Return the first declared branch. (`dif validate` catches these at build time as `W001`; this is the runtime safety net.)
3.  **Disabled or uninitialized:** `dif.init()` wasn't called or `enabled: false`. Return the first variant. No exposure.
4.  **Null user:** `userId()` returned `null`. First variant. No exposure.
5.  **Audience miss:** the predicate returned `false`. First variant. No exposure.
6.  **Resolve:** deterministically bucket the user from the experiment salt and user id, pick the variant by cumulative weight, fire one exposure, return that branch's value.

> // note The control branch is always the first variant declared in the `.md` file. Order your `variants:` list with intent: put the safe fallback first. One exposure fires per `(experiment, user)` for the lifetime of the loaded module.

## Sinks

A sink receives **exposure** events. Five factories ship; you can write your own. Sinks are independent of `dif.track()`. Metric events always go straight to Cloud, never through sinks.

When `publishableKey` is set and you don't pass `sink`, the SDK automatically sends exposures to `POST <apiUrl>/v1/exposure` with a bearer publishable key. Pass `sink: []` to opt out.

```
cloudSink({ apiUrl, publishableKey })   // the default when a publishable key is set
webhookSink(url)                       // POST each event as JSON, keepalive fetch
segmentSink(analytics)                 // analytics.track("dif.exposure", event)
amplitudeSink(amplitude)
mixpanelSink(mixpanel)
```

Combine the Cloud sink with another to fan an exposure out to both:

```
import { dif, cloudSink, segmentSink } from "@dif.sh/sdk";

dif.init({
  publishableKey: "dif_pk_live_…",
  sink: [
    cloudSink({ apiUrl: "https://cloud.dif.sh", publishableKey: "dif_pk_live_…" }),
    segmentSink(window.analytics),
  ],
});
```

### Custom sinks

The `Sink` interface is two fields:

```
import type { Sink } from "@dif.sh/sdk";

const sink: Sink = {
  kind: "custom",             // identifier for logs/dedupe
  emit(event) {                 // must not throw
    customAnalytics.track(event.event, event);
  },
};
```

A sink that throws would crash the render, so swallow errors internally. Pass an array to fan out.

## Exposure events

Every exposure event has the same shape, regardless of sink:

```
interface ExposureEvent {
  event: "dif.exposure";
  experiment: string;
  variant: string;
  user_id: string;
  surface: string;
  bucket: number;          // 0–9999
  fired_at: number;        // unix ms
  source: string;          // SDK source stamp
}
```

### Dedupe

Exposure fires at most once per `(experiment, user_id)` pair for the lifetime of the loaded module. The dedupe set clears on page nav. The point: fire on render, not on assignment, and don't double-count re-renders.

### Why render, not assignment

Bucketing is deterministic. The assignment doesn't change between calls. What you're counting is "this user actually saw the variant." Firing at assignment over-counts users who got bucketed but never rendered (audience misses that ran the predicate, route changes that aborted the render). Firing at render under-counts nothing and over-counts nothing.

## Server assignment

For request-scoped assignment without a browser, import the generated registry and call `assign` with an explicit request context. It returns the assignment without reading the browser singleton, firing an exposure, or touching client dedupe. Safe to call from a long-lived server process.

```
import "./dif/generated/client";
import { assign } from "@dif.sh/sdk";

const assignment = assign("checkout-cta-v2", {
  userId: session.userId,
  attributes: { locale: requestLocale, plan: session.plan },
  overrides: {},
});
```

```
interface Assignment {
  variant: string;
  bucket: number | null;
  exposed: boolean;
  forced?: boolean;
}
```

`assign` returns `null` for an unknown id. Use `registered()` to assign every experiment at once. If the server renders an eligible assignment, the browser records the exposure after mount with `recordExposure(id, variant, bucket)`. It uses the browser SDK's configured user id and sinks and dedupes against later `dif()` calls. The Svelte adapter does this server-to-client handoff for you.

## React adapter

`@dif.sh/react` is a provider + hook layer. `@dif.sh/sdk` is a peer dependency. Install both.

```
$npm install @dif.sh/sdk @dif.sh/react
```

```
import "./dif/generated/client";
import { attributes } from "./dif/generated/audiences";
import { DifProvider } from "@dif.sh/react";

export function Root({ children }) {
  return (
    <DifProvider config={{
      project: "acme-shop",
      publishableKey: process.env.NEXT_PUBLIC_DIF_PUBLISHABLE_KEY,
      userId: () => currentUser?.id ?? null,
      attributes: () => attributes({ plan: currentUser?.plan ?? null }),
    }}>
      {children}
    </DifProvider>
  );
}
```

Props: `config`, `children`, `allowOverrides` (default true), `preview` (default true). `DifProvider` initializes the SDK once on mount and doesn't re-init when `config` changes. Pass a stable config.

Inside the tree, `useDif()` returns `{ track, exposure }`. `exposure` has the same signature as the bare `dif()` function:

```
import { useDif } from "@dif.sh/react";

export function CheckoutCTA() {
  const { exposure } = useDif();
  const cta = exposure("checkout-cta-v2", {
    control: () => "Place order",
    variant_a: () => "Get it today",
  });
  return <button>{cta()}</button>;
}
```

`useDif()` throws outside `<DifProvider>`. On mount the provider reads `?_dif=` or the `_dif` cookie and shows a preview badge when overrides are active; `allowOverrides={false}` disables overrides and `preview={false}` hides only the badge. There's no request-scoped SSR helper yet. Use the pure [`assign`](#assign) API for custom server rendering.

## Svelte adapter

`@dif.sh/svelte` gives SvelteKit request-scoped assignment, a stable anonymous `dif_uid` cookie, header-derived attributes, the server-to-client exposure handoff, and a readable store for experiment values.

```
$npm install @dif.sh/sdk @dif.sh/svelte
```

### 1\. Assign in a server layout

```
// src/routes/+layout.server.ts
import "$lib/dif/generated/client";
import { difLoad } from "@dif.sh/svelte/server";

export const load = (event) => ({ dif: difLoad(event) });
```

`difLoad` reads or creates the `dif_uid` cookie, derives attributes from request headers (`Accept-Language` → `locale`, `User-Agent` → `device_type`), merges your app attributes over the defaults, reads QA overrides, assigns every registered experiment without firing exposures, and returns serializable `DifData`.

```
export const load = (event) => ({
  dif: difLoad(event, {
    attributes: {
      plan: event.locals.user?.plan ?? null,
      returning_visitor: Boolean(event.locals.user),
    },
  }),
});
```

### 2\. Initialize in the root layout

```
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { setContext } from "svelte";
  import { initDif, DIF_CONTEXT_KEY } from "@dif.sh/svelte";
  import { PUBLIC_DIF_PUBLISHABLE_KEY, PUBLIC_DIF_CLOUD_URL } from "$env/static/public";

  let { data, children } = $props();

  setContext(DIF_CONTEXT_KEY, data.dif);
  initDif({
    data: data.dif,
    publishableKey: PUBLIC_DIF_PUBLISHABLE_KEY,
    apiUrl: PUBLIC_DIF_CLOUD_URL,
  });
</script>

{@render children()}
```

`initDif` takes `userId`, `attributes`, and overrides from `data`. You can't pass them directly. It accepts any publishable key, API URL, sink, or `enabled` option you add.

### 3\. Render an experiment store

```
<script lang="ts">
  import { experiment, track } from "@dif.sh/svelte";

  const cta = experiment("checkout-cta-v2", {
    control: () => "Place order",
    variant_a: () => "Get it today",
  });
</script>

<button onclick={() => track("checkout_cta_clicked")}>
  {$cta.value}
</button>
<small>Variant: {$cta.variant}</small>
```

`experiment(id, branches)` returns a `Readable<{ value, variant }>`. Call it during component init (it reads Svelte context). It reuses the server assignment from `DifData` when present, otherwise assigns on the client from the stable cookie, and falls back to the first branch for an unknown experiment. It fires an owed exposure only after a client subscription. That keeps the server HTML and the first browser render aligned.

> // note The `dif_uid` cookie holds a random UUID (not a secret), is readable by the client (`httpOnly: false`) so browser bucketing matches the server, uses `path: /`, and has a one-year max age. For local plain-HTTP dev that rejects secure cookies, pass `difLoad(event, { secure: false })`. Both `difLoad` and `initDif` honor `?_dif=…` preview links.

* * *

## dif.track() Cloud

Fire a metric event. Cloud associates it with whatever variant the same `user_id` was bucketed into, so you compute lift without writing any join code.

```
import { dif } from "@dif.sh/sdk";

// the simplest call — a count event
dif.track("completed_checkout");

// with a value (revenue, duration, count, …)
dif.track("completed_checkout", {
  value: 49.00,
  currency: "USD",
});

// with extra dimensions
dif.track("signup", {
  props: { plan: "pro", referrer: "hn" }
});
```

| argument | type | notes |
| --- | --- | --- |
| `metric` | string | The metric slug. A new slug lands in the Cloud Metrics catalog the first time it's seen. |
| `value` | number? | Revenue, duration, or count: any numeric quantity to aggregate over. |
| `currency` | string? | ISO-4217 code (e.g. `"USD"`). Pair with `value` for revenue. |
| `unit` | string? | Free-form unit (e.g. `"ms"`, `"items"`) for duration / count metrics. |
| `userId` | string? | Override. Defaults to `userId()` from `dif.init()`. |
| `firedAt` | number? | Unix ms. Defaults to now. |
| `idempotencyKey` | string? | When present, Cloud deduplicates by `(project, key, fired_at)`. Safe to retry from a failing route handler. |
| `props` | object? | Arbitrary JSON-serializable extras, stored for later breakdowns. |

### Drop conditions & transport

The call is silently dropped (no throw, no retry) when:

-   `dif.init()` hasn't run, or `enabled: false`;
-   `userId()` returns `null` (no one to attribute to);
-   `publishableKey` isn't set: the event is logged with `console.debug` and dropped.

Browser calls post one event at a time to `POST <apiUrl>/v1/track` with `fetch(..., { keepalive: true })` so the request survives page nav. Network failures are swallowed and warned. Bad analytics must not crash a render.

## Server SDK Cloud

Server-side metric tracking lives at the `@dif.sh/sdk/server` subpath. Use it from route handlers, server actions, or queue workers: anywhere you have a secret API key instead of a publishable key.

```
import { DifServer } from "@dif.sh/sdk/server";

const dif = new DifServer({
  apiKey: process.env.DIF_API_KEY,
  apiUrl: "https://cloud.dif.sh",
});

await dif.track({
  metric: "completed_checkout",
  userId: user.id,
  value: 49.00,
  currency: "USD",
  idempotencyKey: order.id,
});
```

| field | type | notes |
| --- | --- | --- |
| `apiKey` | string | Required secret token (`dif_live_…` / `dif_test_…`). **Never ship in a browser bundle.** |
| `apiUrl` | string? | Defaults to `https://cloud.dif.sh`. |
| `source` | string? | Overrides the default source stamp. |

`DifServer.track(...)` takes the same options as `dif.track()`, except `userId` is required: servers don't have a configured user resolver, so you pass it per call. It sends one request per call and does not batch or retry; non-success responses and network errors produce `console.warn` output without rejecting the call.

## Publishable keys Cloud

Cloud has two token types. Pick the right one for where the code runs.

| kind | format | where it runs | what it can do |
| --- | --- | --- | --- |
| `dif_pk_…` | publishable | browser bundles, mobile apps | POST to `/v1/track` and `/v1/exposure` only. |
| `dif_live_…` / `dif_test_…` | secret | servers, CI, scripts | All ingest endpoints. **Never embed in a browser bundle.** |

> // cloud Publishable keys are intentionally low-privilege but not zero-privilege: anyone who reads your JS bundle can fire events for your project. For server traffic always use a secret key, and never log it.

## Ingest endpoints Cloud

If you'd rather not use the SDK, post events to Cloud's HTTP endpoints directly. Same shape, same bearer auth.

### POST /v1/track

```
POST https://cloud.dif.sh/v1/track
Authorization: Bearer $DIF_API_KEY
Content-Type: application/json

{
  "metric": "completed_checkout",
  "user_id": "u_8131",
  "value": 49.00,
  "currency": "USD"
}
```

### POST /v1/exposure

```
POST https://cloud.dif.sh/v1/exposure
Authorization: Bearer $DIF_API_KEY
Content-Type: application/json

{
  "event": "dif.exposure",
  "experiment": "checkout-cta-v2",
  "variant": "variant_a",
  "user_id": "u_8131",
  "surface": "checkout",
  "bucket": 7142,
  "fired_at": 1716304931542
}
```

The exposure shape matches the SDK's `ExposureEvent` 1:1, useful when forwarding from a custom sink or backfilling. Add `"idempotency_key"` to any event and Cloud deduplicates by `(project, key, fired_at)`, so a retried POST is safe.
