⚠️ Work in progress — createCMS is pre-1.0 and not production-ready (not tested in production). Expect breaking changes.
createCMS

A/B testing

Deterministic variant assignment, event tracking, and pluggable analytics.

The A/B testing plugin runs experiments on content. It assigns visitors to variants deterministically, records impressions and conversions from block events, and forwards results to analytics. It has a server half and a client half.

Installation

Add the server plugin

lib/cms.ts
import { createCMS } from '@createcms/core';
import { abTest } from '@createcms/core/plugins/ab-test';

export const cms = createCMS({
  db,
  collections,
  media,
  plugins: [abTest()],
});

Update the database

The plugin adds tables for tests, variants, and events. Regenerate the schema file, then apply it to your database with your Drizzle migration workflow:

npx createcms generate
npx drizzle-kit generate && npx drizzle-kit migrate

Add the client plugin

lib/cms-client.ts
import { createCMSClient } from '@createcms/core/react';
import { abTestClient } from '@createcms/core/plugins/ab-test/client';
import type { cms } from './cms';

export const cmsClient = createCMSClient<typeof cms>()({
  baseURL: '/api/cms',
  plugins: [abTestClient()],
});

Usage

The client exposes an abTest namespace. Record an impression for the variant a visitor sees, read their assignment, and manage consent:

cmsClient.abTest.recordImpression(testId, branchId);

const { variantId, branchId, assignedAt } = await cmsClient.abTest.getVariant(testId);

cmsClient.abTest.setConsent({ analytics_storage: 'granted' });
ActionSignaturePurpose
recordImpression(testId, branchId) => voidRecord that a variant was shown.
useImpression(testId, branchId) => voidReact hook form of recordImpression.
getVariant(testId) => Promise<{ variantId, branchId, assignedAt }>Read the visitor's assignment.
setConsent / getConsentConsent Mode v2 signalsGate analytics on consent.
dispatchEvent(event) => voidDispatch a client event.
identify / resetvisitor contextSet or clear the visitor key.

Live results

useLiveResults streams result deltas to the dashboard over the public ab:live:<testId> channel. It's imported from its own subpath (which pulls in the optional @upstash/realtime peer) and rides the shared RealtimeProvider connection — the same one useNotifications uses:

import { useLiveResults } from '@createcms/core/plugins/ab-test/live';

const { results, isLive } = useLiveResults({
  testId,
  initial, // SSR snapshot from getResults
  getResults: () => client.abTest.getResults({ query: { testId } }), // reconcile on (re)connect
});

Live delivery is decoupled from the analytics storage adapter — it works with the Postgres or Upstash adapter, as long as realtime is configured on the server. Without it the stream never connects and initial (+ any getResults reconcile) stands.

Schema

TablePurpose
ab_testsOne row per test: target root, goal, status (draft, running, paused, completed), traffic percentage.
ab_test_variantsThe variants of a test, each tied to a branch, with a weight and a control flag.
ab_test_eventsThe analytics event store. eventType is free-form (impression, conversion, form_submit, page_view, and more); testId and variantId are nullable.

Options

Server, abTest(options)

OptionTypeDefaultDescription
analyticsABTestAnalyticsAdapterpostgresAnalytics()Where events are stored or forwarded.
ga4GA4ServerConfigServer-side GA4 Measurement Protocol forwarding.
rateLimitABTestRateLimitOptionsRate-limit the anonymous event ingest.

Client, abTestClient(options)

OptionTypeDefaultDescription
disableDataLayerSinkbooleanfalseStop forwarding to the browser dataLayer (use when server-side GA4 is configured).

Client-side vs server-side measurement

By default the client forwards events to the browser dataLayer (for GTM/GA4). If you instead configure server-side forwarding with the ga4 option, set disableDataLayerSink: true on the client so the same event is not counted twice.

For edge variant assignment in middleware, use the Next.js A/B primitives from @createcms/core/next/middleware and @createcms/core/ab-edge (see Edge A/B).

On this page