@coframe/sdk
Bring your UX to life with AI-powered optimization and personalization.
Coframe helps you personalize and optimize your site with experiments, targeting, anti-flicker rendering, analytics callbacks, and conversion tracking.
This package exposes three entry points:
@coframe/sdkfor the browser runtime@coframe/sdk/serverfor SSR and edge prefetching@coframe/sdk/reactfor React apps
Install
npm install @coframe/sdk
If you use the React entry point, install React as well:
npm install react@">=17"
Quick start
Browser
import { init } from '@coframe/sdk'
await init({
config: {
projectId: 'your-project-id',
},
})
init() accepts config and optional projectData, so npm consumers do not need to manually populate window.CFQ first.
Runtime endpoint overrides
Private-CDN integrations can override the browser SDK's project and ingest endpoints:
await init({
config: {
projectId: 'your-project-id',
projectUrl: 'https://cdn.example.com/coframe/project.json',
ingestUrl: 'https://www.example.com/ingest/v2',
replayScriptUrl: 'https://www.example.com/replay/latest.js',
enableDebug: false,
},
})
projectUrl must return a raw, complete Project JSON object. ingestUrl
replaces the ingest base URL (https://ingest.app.coframe.com/ingest/v2); the
SDK still appends the category and project path, so beacons go to
<ingestUrl>/batched_events/<projectId>/, <ingestUrl>/batched_monitoring/<projectId>/,
<ingestUrl>/session_result/, and <ingestUrl>/identify/<projectId>/. A proxy
only has to forward <ingestUrl>/* to https://ingest.app.coframe.com/ingest/v2/*
with the path unchanged. Both overrides are optional and retain the compiled
Coframe endpoints when omitted.
replayScriptUrl replaces the session replay recorder script URL. A proxy must
forward it to <edge>/replay/latest.js unchanged.
Configuring either projectUrl or ingestUrl makes CMAB experiments use
deterministic local assignment without calling the Edge assignment API.
Configure all three overrides when the browser must avoid the SDK-compiled
Coframe project, ingest and replay script endpoints entirely. Debug tools
remain controlled by enableDebug.
URL controls
The npm entry uses the same browser URL controls as the script-tag and Edge installations:
cf_disableskips initialization and reveals any antiflicker state.coframe_variant_idpreviews one or more comma-separated variant IDs. Addrolloutsto the list to apply them on top of the rolled-out winners a production visitor would get.coframe_should_send_events=trueallows events during preview for testing.
Debug tools are allowed by default. A preview link (coframe_variant_id with
cf_preview=true) keeps the selected variants active for the current tab as you
navigate; turn off Keep across navigation in the Preview Session tool to
stop. Set enableDebug: false to prevent URL or saved session state from
loading the Launcher (a preview link then applies to its own page only).
For SSR, pass the complete request URL, including its query string, as
currentUrl. This is required for query-sensitive targeting and allows
getProjectData() to short-circuit before any request when cf_disable is
present.
React
import { CoframeProvider } from '@coframe/sdk/react'
import type { CoframeConfig } from '@coframe/sdk'
const projectConfig: CoframeConfig = {
projectId: 'your-project-id',
}
export default function App({ children }) {
return (
<CoframeProvider projectConfig={projectConfig}>
{children}
</CoframeProvider>
)
}
Mount the provider once at the root of your app. It initializes the SDK on mount and keeps treatment state available to child components. Keep it mounted across client-side route changes; runtime commands and the SDK's navigation observer handle subsequent page changes without remounting it.
Server prefetch
import { getProjectData } from '@coframe/sdk/server'
const serverData = await getProjectData({
projectId: 'your-project-id',
currentUrl: request.url,
headers: request.headers,
})
const projectData = serverData?.project
? {
project: serverData.project,
personalizationHeaders: serverData.personalizationHeaders,
}
: undefined
Pass projectData into <CoframeProvider> to avoid the extra client-side project fetch and personalize on first paint.
Server-side assignment
Use getExperimentAssignment() when the server must know one experiment's
variant before rendering or calling an upstream service. Edge performs the full
assignment; the package never falls back to local bucketing.
import {
generateUserToken,
getExperimentAssignment,
} from '@coframe/sdk/server'
// Next.js `request.cookies.get()` returns `{ name, value }`; other runtimes
// return the string. Either way `userToken` must be the string value.
const existingToken = request.cookies.get('coframe.user.token')?.value
const userToken = existingToken ?? generateUserToken()
if (!existingToken) {
// Do not set `httpOnly`: the browser SDK reads this cookie to reuse the
// same identity. An HttpOnly token makes the browser generate a new one and
// bucket differently, with no error.
response.cookies.set('coframe.user.token', userToken, {
path: '/',
secure: true,
sameSite: 'lax',
maxAge: 400 * 24 * 60 * 60,
})
}
const assignment = await getExperimentAssignment({
projectId: 'your-project-id',
experimentId: 'your-experiment-id',
userToken,
currentUrl: request.url,
headers: request.headers,
})
const upstreamVersion =
assignment?.variant?.slug === 'variant-2' ? 'v2' : 'v1'
The package derives visitor context from an allowlist of request headers.
Explicit userAgent, languages, browserPlatform, country, region,
city, device, botScore, verifiedBotCategory, clientIp,
userVisitCount, and userContext options override the derived values. Only
the lane cookie is forwarded to Edge; authorization and application cookies are
not forwarded. clientIp is used only for existing blocklist targeting.
Geography, device, bot score, and client IP are read from cf-* request
headers. Cloudflare adds cf-ipcountry and cf-connecting-ip to every
request that reaches your origin; cf-region-code and cf-ipcity need the
"Add visitor location headers" Managed Transform, cf-device-type needs
"cache by device type", and cf-bot-score needs Bot Management plus a
Transform Rule. If your server is not behind Cloudflare, or a header is not
enabled, that value is missing: the country becomes unknown, country
audiences never match, and the device falls back to the user agent. Pass
country, region, city, and device explicitly in that case, from
whatever geo source your stack already has. Values are forwarded verbatim and
audiences compare them exactly, so use Cloudflare's spelling: country is the
uppercase ISO 3166-1 alpha-2 code (US, not us), region the ISO 3166-2
subdivision code (CA), and device one of desktop, mobile, tablet.
When a result is available, pass its project snapshot to the provider. The browser reads the same standard token cookie and runs its existing assignment flow without fetching project data again.
<CoframeProvider
projectConfig={{ projectId: 'your-project-id' }}
projectData={assignment?.projectData}
>
{children}
</CoframeProvider>
assignment is undefined when the call failed (see below), so read
projectData with optional chaining and decide there whether the browser may
initialize on its own.
generateUserToken() uses Web Crypto when available and falls back to a
non-cryptographic UUID generator in server runtimes without Web Crypto. The
token is assignment identity, not an authentication credential. It remains
stable until its cookie is removed, expires, or is replaced.
The separate visit count uses the SDK's existing two-hour visit boundary. The
package only reads the visit cookies the browser SDK writes (coframe.cvc,
coframe.lastVisitTime) and never sets them, so without the browser SDK running
after consent the count stays at 1 and returning audiences never match.
The result contains either one variant or variant: null with a structured
reason (experiment_not_found; not_assigned, which also covers a visitor
held out of this experiment or of the whole project by a holdout and renders
the default experience in the browser too; unsupported_experiment_type for
CMAB targets; preview_active for URL previews; a newer Edge may add reasons,
so treat any reason you do not know as "no variant"). Both forms include the exact projectData snapshot used by Edge, so the
browser can initialize from that snapshot. The snapshot also carries the
normalized language, platform, and visit-count values used by Edge. The browser
uses those values only for its initial assignment and returns to its normal
browser-derived context for later SPA navigations. In every case without a
variant, use the default upstream experience.
The function returns undefined for transport, timeout (timeoutMs,
default 1500 ms), Edge, or response validation failures, and when
currentUrl carries cf_disable. Handle that state explicitly. Invalid options are a
programming error and throw instead: an empty projectId, experimentId or
userToken, a currentUrl that is not an absolute URL (Express and Fastify
req.url is a path, build the full URL first), out-of-range languages,
device, botScore, timeoutMs and userVisitCount values, any string
option longer than its limit (4096 characters for URLs, 2048 for the user
agent, 200 for projectId, 512 for experimentId and userToken), or a
userContext that serializes to more than 4096 bytes (userContext is too large). Values derived from
request headers are clamped instead. To preserve server/browser
consistency for the response, do not let the browser independently fetch project
data and assign an experiment after the server has fallen back to its default.
Applications that accept that possible mismatch may instead initialize the
browser normally as their fail-open policy.
What Edge cannot evaluate
Server assignment runs the same deterministic logic as the browser, with the same token, the same project snapshot, and the same visitor context. The browser then reaches the same bucket because every input is the same. Five kinds of input only exist in the page, so Edge cannot evaluate them:
- JavaScript targeting. A
js_functioncondition (custom JavaScript that reads a page value, a cookie,localStorage, or thedataLayer) runs only in the browser, whether it sits on an audience or on a frame's activation condition. Edge treats it as not matched.- If the target experiment itself uses a JavaScript audience, the result is
variant: nullwith reasonnot_assigned. Do not use JavaScript audiences on experiments you resolve on the server. - Experiments in the same frame are evaluated in order and the first match wins. If a JavaScript-targeted experiment sits ahead of the target in the same frame, Edge skips it and assigns the target, while the browser can match it and skip the target. Keep JavaScript-targeted experiments out of the target experiment's frame, or place them after the target.
- The same applies to a JavaScript-targeted experiment that shares a lane with the target, wherever it sits: lane winners are drawn from the experiments the visitor can reach, and Edge cannot tell whether that one is reachable. Keep JavaScript-targeted experiments out of the target's lane.
- If the target experiment itself uses a JavaScript audience, the result is
- CMAB experiments. A target CMAB experiment returns
variant: nullwith reasonunsupported_experiment_type. - URL fragments. A server never sees the
#fragmentof the page URL, and the browser re-evaluates the snapshot with its fulllocation.href. Frames that usebase_url_matchesignore query and fragment and are safe; an exacturlcondition compares both, and aurl_regexruns against the fulllocation.hrefin the browser, so pass the same URL the browser will have, without a fragment, and avoid regexes anchored on the end of the URL. - Previews. Previews are browser-only. A
coframe_variant_idURL parameter reaches Edge, but the published preview payloads do not name the experiment a variant belongs to, so the server answersvariant: nullwith reasonpreview_activeand the browser renders the preview from the returned snapshot. A preview kept across navigation by the Preview Session tool lives in the browser'ssessionStorageand never reaches Edge; the browser discards the snapshot and fetches its own project. Both affect preview users only; render the default upstream experience. - Lane state. Edge reads lane claims from the
coframe.lanescookie only. Before cookie consent the browser keeps claims inlocalStorage, which the server cannot see, and the server does not persist the claims Edge commits. Laned experiments therefore agree once the browser has written the cookie, not for a visitor without cookie consent.
Audience conditions based on URL (base_url_matches), country, device,
language, platform, visit type, search, time, allocation, or userContext
evaluate identically on both sides, as does bot cloaking. For platform, Edge derives the same
navigator.platform family the browser reports (a Linux … string on Android,
for example Linux armv8l), or takes an explicit browserPlatform option.
If the application uses a custom identity cookie, call setUserToken() before
browser initialization with waitForUserToken: true. That integration must also
keep visit and lane state aligned between the server and browser.
Feature flags and remote config
Browser Feature Flags use the existing public projectId and receive only
flags marked Public. selector is optional; use names for
individual flag keys and groups to evaluate every flag assigned to a page or
business area.
import { setUserToken } from '@coframe/sdk'
import {
featureFlags,
getFeatureFlag,
getFeatureFlagPayload,
isFeatureEnabled,
onFeatureFlags,
} from '@coframe/sdk/feature-flags'
setUserToken('customer-123')
await featureFlags.init({
projectId: 'your-project-id',
// Optional: replaces the complete default ingest base, including /ingest/v2.
ingestUrl: 'https://events.example.com/coframe',
selector: {
names: ['checkout-redesign'],
groups: ['homepage_campaigns'],
},
})
onFeatureFlags(() => {
if (isFeatureEnabled('checkout-redesign')) {
showNewCheckout()
}
const content = getFeatureFlagPayload('homepage-content')
const checkoutVariant = getFeatureFlag('checkout-layout')
})
Feature Flags are a separate browser entry point and do not increase or initialize
the main SDK bundle. When both clients are used, Feature Flags reuse the latest
setUserToken() command from Coframe's shared command queue and follow later
identity changes automatically.
Loading does not delay Coframe page modification. Synchronous getters return
undefined until a cached or fresh evaluation is available. The browser client
always keeps the latest successful response in memory. Before consent, no
Feature Flag result or evaluation context is persisted. After consent, the
client also caches the response in localStorage for five minutes and keys it
with an opaque SHA-256 fingerprint derived from the user token and evaluation
context. It can fall back to that persistent cache if Edge is temporarily
unavailable. Call reloadFeatureFlags() to request fresh evaluations
explicitly.
Every single-flag getter automatically records a feature_flag_called event
after a result has loaded. This applies to boolean, multivariate, and Remote
Config flags. The event includes the key, evaluated value, payload, version,
reason, and evaluation request metadata. The project, key, and version identify
the corresponding flag definition without changing the Edge response. Every
getter or hook access records its own event, including repeated reads of the
same result. To read without recording an event, pass { sendEvent: false }:
const variant = getFeatureFlag('checkout-layout', { sendEvent: false })
Tracking uses an independent Feature Flags queue and does not initialize the
main SDK. It follows the shared consent state: while consent is explicitly
disabled, events remain in memory and are sent after consent is enabled. The
optional Feature Flags ingestUrl has the same base-replacement behavior as
the main SDK; the client appends /batched_events/<projectId>/.
Payloads support any JSON value up to 32 KB. A payload is returned only when its flag is enabled. Remote Config uses the same transport and payload API and returns the same payload to all users while active:
import { getRemoteConfig } from '@coframe/sdk/feature-flags'
const navigation = getRemoteConfig('navigation-config')
Multivariate flags return a stable variant key from getFeatureFlag(). Their
weights divide users into deterministic buckets, and each variant can carry an
independent optional payload. isFeatureEnabled() remains a boolean
convenience and returns true whenever a variant is assigned:
import {
getFeatureFlag,
getFeatureFlagPayload,
isFeatureEnabled,
} from '@coframe/sdk/feature-flags'
const variant = getFeatureFlag('checkout-layout')
const config = getFeatureFlagPayload('checkout-layout')
const enabled = isFeatureEnabled('checkout-layout')
React apps can read evaluated values from the existing provider:
import {
useFeatureFlag,
useFeatureFlagEnabled,
useFeatureFlagPayload,
useFeatureFlagVariantKey,
} from '@coframe/sdk/react'
function Checkout() {
const enabled = useFeatureFlag('checkout-redesign')
const payload = useFeatureFlagPayload('checkout-redesign')
if (enabled === undefined) return <CheckoutSkeleton />
return enabled ? <NewCheckout config={payload} /> : <OldCheckout />
}
function CheckoutLayout() {
const variant = useFeatureFlagVariantKey('checkout-layout')
const enabled = useFeatureFlagEnabled('checkout-layout')
const payload = useFeatureFlagPayload('checkout-layout')
return <Checkout variant={variant} enabled={enabled} config={payload} />
}
For server and edge runtimes, use a secret Feature Flags Server API key. It is
project-scoped, can read Private flags, and must never be included in a
browser bundle or a NEXT_PUBLIC_* environment variable. evaluateFlags()
records every flag returned by its selector, while each single-key helper
records only that key. Repeated evaluations record new events even when the
result is unchanged. Both paths enqueue tracking without delaying their result
and accept sendEvent: false to evaluate without recording an event.
Single-key helpers send a key-filtered request and fail closed unless a
defaultValue is supplied. Browser and server helpers both call
/api/v1/flags; server helpers include the API key as a Bearer credential so
the response may include both Public and Private flags:
import {
evaluateFlags,
flushFeatureFlagEvents,
getFeatureFlag,
getRemoteConfig,
isFeatureEnabled,
} from '@coframe/sdk/server'
const options = {
projectId: 'your-project-id',
apiKey: process.env.COFRAME_FEATURE_FLAGS_SERVER_KEY!,
userToken: user.id,
currentUrl: request.url,
userContext: { plan: user.plan },
}
const checkoutFlags = await evaluateFlags({
...options,
selector: { groups: ['cart_checkout'] },
ingestUrl: 'https://events.example.com/coframe',
})
// evaluateFlags tracks every returned flag without another Edge request.
// Single-key helpers track their requested key automatically.
const checkoutVariant = await getFeatureFlag('checkout-layout', {
...options,
ingestUrl: 'https://events.example.com/coframe',
})
// Useful before a short-lived process exits. Tracking failures are swallowed.
await flushFeatureFlagEvents()
const enabled = await isFeatureEnabled('checkout-redesign', {
...options,
defaultValue: false,
})
const variant = await getFeatureFlag('checkout-layout', options)
const content = await getRemoteConfig('homepage-content', options)
Common React usage
Read variants in components
'use client'
import { useCoframeVariant } from '@coframe/sdk/react'
export function CheckoutPage() {
const variant = useCoframeVariant('checkout_flow_v2')
if (variant === undefined) return <CheckoutSkeleton />
if (variant === 'treatment') return <NewCheckout />
return <OldCheckout />
}
Other hooks:
useCoframeTreatments()returns all treatments ornullwhile init is in flightuseCoframeTreatment(experimentId)returns one treatment payload
Track conversions
'use client'
import { trackConversion } from '@coframe/sdk'
export function CheckoutButton() {
return (
<button
onClick={() =>
trackConversion({
metricName: 'checkout_complete',
value: 99.95,
})
}
>
Complete checkout
</button>
)
}
Analytics and log callbacks
import { CoframeProvider } from '@coframe/sdk/react'
<CoframeProvider
projectConfig={projectConfig}
onTreatmentAppliedBulkV2={(data) => {
for (const treatment of data) {
analytics.track('Experiment Viewed', treatment)
}
}}
onLog={(payload) => {
console.error('[coframe]', payload.monitoring_type, payload)
}}
>
{children}
</CoframeProvider>
If your privacy policy permits treatment analytics before cookie consent, use
onTreatmentAppliedBulkV2WithoutConsent instead. It has the same callback
arguments and does not fire again when consent is later granted.
Outside React, you can also register callbacks from the main entry:
import { onLog, onTreatmentApplied } from '@coframe/sdk'
Next.js / Remix / SSR notes
getProjectData()is intended for SSR and edge runtimes that can provide a full request URL and request headers.- In Pages Router or Remix, per-request server loaders are the most straightforward place to call it.
- In App Router, keep the provider mounted in a persistent shell, but be careful about request URL handling in
layout.tsxif you need query-sensitive SSR targeting. - The npm runtime reveals antiflicker styles but cannot insert them early enough to protect the initial paint. Render
antiflickerCSSfromgetProjectData()in the server document head, or place equivalent static CSS and a timeout fallback there before application content.
Package layout
The published tarball contains only dist/pkg/:
dist/pkg/
├── cf.esm.mjs cf.cjs.js types/index.d.ts ← @coframe/sdk
├── server.esm.mjs server.cjs.js types/server.d.ts ← @coframe/sdk/server
└── react.esm.mjs react.cjs.js types/react.d.ts ← @coframe/sdk/react
TypeScript compatibility
The package publishes typed subpath exports for:
@coframe/sdk@coframe/sdk/server@coframe/sdk/react
If your app uses an older TypeScript module resolution mode, prefer moving to one of:
moduleResolution: "bundler"moduleResolution: "node16"moduleResolution: "nodenext"
Modern resolution modes handle package subpath exports more reliably.
API overview
@coframe/sdk
init(options)trackConversion(payload)setConsent(enabled)setUserToken(token)setFeatureFlag(enabled)setPageHydrated()onTreatmentApplied(...)onLog(...)
@coframe/sdk/feature-flags
featureFlags.init(options)featureFlags.setUserToken(userToken)/featureFlags.reset()featureFlags.getUserToken()getFeatureFlag(key)/isFeatureEnabled(key)getFeatureFlagPayload(key)/getRemoteConfig(key)onFeatureFlags(callback)/reloadFeatureFlags()
@coframe/sdk/server
getProjectData(options)getExperimentAssignment(options)generateUserToken()evaluateFlags(options)getFeatureFlag(key, options)/isFeatureEnabled(key, options)getFeatureFlagPayload(key, options)/getRemoteConfig(key, options)flushFeatureFlagEvents()
Returns a payload that may include:
projectpersonalizationHeadersantiflickerCSS
@coframe/sdk/react
CoframeProvideruseCoframeTreatments()useCoframeTreatment(experimentId)useCoframeVariant(experimentId)useFeatureFlag(key)/useFeatureFlagVariantKey(key)useFeatureFlagEnabled(key)useFeatureFlagResult(key)/useFeatureFlagPayload(key)