# splithub.config.ts

The config is a plain TypeScript (or JavaScript) module that exports `defineConfig(...)` from `@splithub/core`. `defineConfig` validates and normalizes it at import time, so mistakes fail fast with a readable message. The file ships to the browser: never put secrets in it.

```ts
import { defineConfig } from '@splithub/core';

export default defineConfig({
  project: 'my-app',
  storage: { mode: 'cloud', key: 'pk_live_…' },
  runDraftsInDev: true,
  goals: {
    signup: { type: 'event' },
    purchase: { type: 'event', revenue: true },
    pricing_view: { type: 'pageview', path: '/pricing' },
    cta_click: { type: 'click', selector: '#cta' },
  },
  experiments: {
    'hero-title': {
      kind: 'value',
      status: 'running',
      goal: 'signup',
      description: 'Feature-led vs outcome-led headline',
      variants: {
        control: { weight: 50, value: 'Ship experiments from your terminal' },
        outcome: { weight: 50, value: 'Know which version wins, by Friday' },
      },
    },
    'pricing-cta': {
      kind: 'component',
      status: 'draft',
      goal: 'purchase',
      traffic: 0.5,
      variants: { control: 1, 'see-plans': 1, 'talk-to-us': 1 },
      maxExposures: 20000,
      schedule: { startAt: '2026-11-01T00:00:00Z', endAt: '2026-11-30T00:00:00Z' },
    },
    'landing': {
      kind: 'redirect',
      status: 'running',
      goal: 'signup',
      match: '/',
      variants: { control: { weight: 50, to: '/' }, 'new-landing': { weight: 50, to: '/start' } },
    },
  },
});
```

## Top level

| Field | Required | Meaning |
|---|---|---|
| `project` | yes | Name shown in the CLI and the dashboard. |
| `storage` | yes | `{ mode: 'self', endpoint }` or `{ mode: 'cloud', key, endpoint? }`. |
| `goals` | no | Map of goal key → goal. |
| `experiments` | yes | Map of experiment key → experiment. |
| `visitorKey` | no | Cookie name for the visitor id, default `sh_vid`. |
| `runDraftsInDev` | no | `true` serves `draft` experiments like running ones while the app runs in development. Default `false`. |

`storage.endpoint` in self mode is the path (or URL) where the `@splithub/node` handler is mounted. In cloud mode `key` is the project's public key; `endpoint` defaults to `https://go.splithub.io/v1/events` and only changes for a self-hosted cloud or staging.

## Drafts in development

A `draft` experiment is invisible to visitors: everyone sees the control and nothing is recorded. To see it in your own app before you start it, set `runDraftsInDev: true`. While `NODE_ENV` is `development` (`next dev`, `vite`), drafts behave exactly like running experiments: variants by visitor, targeting, redirects, exposures and goals. A production build ignores them as before, so the same config is safe to deploy.

In development the schedule of a draft is ignored, so a test that starts next week can be checked today. In cloud mode development never sends events to the cloud, for drafts and running tests alike, so your own visits never reach the results and nothing is billed. Deployed builds, and `vite preview`, send as usual. While no experiment in the project is running the SDK holds goals back too, because the cloud would drop them.

In development the SDK also explains itself, one line per experiment: a draft that everyone sees as the control, a draft shown because of this option and which variant you got, a paused test, events held back. Next.js server code prints it in the terminal that runs `next dev`. In a Vite app the SDK runs in the browser, so add the plugin to see it in the terminal that runs `vite`:

```ts
// vite.config.ts
import { splithub } from '@splithub/core/vite';

export default defineConfig({ plugins: [react(), splithub()] });
```

On start and on every save of the config it prints each experiment and what it does in development, then forwards the SDK's notes from the browser. Without the plugin the notes stay in the browser console. Production builds print nothing. With `?sh_debug=1` the debug panel shows such assignments with the reason `dev`; its "new visitor" button lets you walk through every variant. `npx splithub init` turns the option on for new projects.

## Goals

| Type | Fields | Fires when |
|---|---|---|
| `event` | `revenue?: boolean` | your code calls `track('<key>', props?)`; with `revenue: true` the `props.revenue` number is summed per variant |
| `pageview` | `path` | a page with that path is viewed (exact match, or prefix with a trailing `*`) |
| `click` | `selector` | an element matching the CSS selector is clicked (the SDK and the drop-in script bind it) |

Add `description` to any goal; the dashboard shows it. Goals created in the dashboard reach cloud clients. Add their definitions to the local file manually; `pull` adopts experiments, not goals.

## Experiments

Keys are lowercase `a-z 0-9 - _`, 1–64 characters. The **first variant is the control**.

| Field | Default | Meaning |
|---|---|---|
| `kind` | required | `value`, `component` or `redirect` |
| `status` | required | `draft`, `running`, `paused`, `finished` |
| `variants` | required | see below |
| `goal` | none | the goal the verdict is computed on; must exist in `goals` |
| `traffic` | `1` | share of visitors (0–1) entering the experiment; others see the control without an exposure |
| `seed` | the key | hash seed; never change it on a running experiment |
| `description` | none | shown in the dashboard and in emails |
| `match` | required for `redirect` | path the redirect applies to (exact, or prefix with trailing `*`) |
| `maxExposures` | none | the cloud finishes the experiment when stored exposure events reach this number |
| `schedule` | none | `{ startAt?, endAt? }` ISO dates; outside the window the control is served and nothing is recorded |
| `flow` | none | targeting graph, see [Targeting flows](/docs/targeting) |

### Variants by kind

```ts
// component: weights only, JSX lives in the app
variants: { control: 1, bold: 1 }

// value: weight + payload (string, number, boolean, object, array)
variants: { control: { weight: 50, value: 'Old' }, bold: { weight: 50, value: 'New' } }

// redirect: weight + destination
variants: { control: { weight: 50, to: '/' }, v2: { weight: 50, to: '/home-v2' } }
```

Weights are relative: `{ a: 1, b: 1 }` and `{ a: 50, b: 50 }` are the same split; `{ a: 90, b: 10 }` is a 90/10 split.

## What the CLI changes

`create` adds an experiment, `start`/`pause`/`finish` change `status`, `pull` adds missing dashboard-created experiments (including flows), but not goal definitions, `init` writes the file the first time. Edits keep your formatting where possible; the file stays a normal TypeScript module you can edit by hand.

## Rules

- Never change `seed`, `variants` or weights of a running experiment; create a new one.
- Keep one `id` for things that must change together, separate ids for independent questions.
- On low traffic run at most 2–3 experiments per page.
- Keys that exist in the file are owned by the file: the dashboard cannot edit them (it can pause or finish them).
- In cloud mode a status set in the dashboard wins over the file's `status`, from a visitor's first page, until you clear it. The dashboard shows both when they differ.
