Open-source experimentation SDK
Run A/B tests without injecting JavaScript into the page.
A variant is bounded data: resolved on the server, rendered by your own React components. Engineers declare typed slots once. Marketing fills and tests them within those bounds, with no new code.
MIT-licensed core. Built for the Next.js App Router.
// The eng <-> marketing contract. One source of truth.
import { slot } from '@apertio/core'
export const slots = {
'home.hero.headline': slot.text({
default: 'Ship experiments without the flicker',
maxLength: 60},
'home.hero.layout': slot.enum({
options: ['centered', 'split'] as const, default: 'centered'},
} // Server Component: resolve the assigned variant, then render.
const { states } = await resolveExperiments({ slots, configSource })
return (
<ApertioProvider states={states}>
<Hero /> // your component, your markup
</ApertioProvider>) The tradeoff today
Most experimentation tools make one of these three groups lose.
Experimentation is bought by marketing, engineering, and security, and today's tools force a choice between them. Bounded structured content satisfies all three at once.
Visual editors
Marketing autonomy, no waiting on engineering.
They inject third-party JavaScript, mutate the live DOM, and force the page to flicker. A strict CSP breaks.
Flag / SDK tools
Safe, no injected scripts, engineering stays in control.
Every test is code. Marketing files a ticket and waits on engineering for each experiment.
Apertio
A variant is bounded data your own components render. Marketing tests within typed slots. No new code, no injected JS, CSP intact.
No tradeoff
The slot primitive
Model a surface once and marketing can test it indefinitely.
The word config hides two things that move in opposite directions. Schema is the slot definitions: the contract, the allowlist, the types. It lives in code. Values are what marketing types: runtime data, validated against that schema. The code/platform line and the eng/marketing line are the same line.
slots.ts
A typed enum slot, owned by engineering. Changes only on deploy. This is the allowlist.
layout: 'centered' | 'split'
Marketing fills it
The editor only offers values the schema allows. Bounded by construction.
- centered
- split
- marquee
rejected · out of bounds
Resolved before render
Assignment is a deterministic hash. Resolution is a pure function of assignment and config.
layout ⇒ 'split'
<Hero />
Your own component consumes the value map. Apertio never touches the DOM. The CSP stays intact.
in server HTML · first paint
-
Engineering declares a typed slot, once.
A slot is a text, enum, number, or boolean field with a mandatory default and explicit bounds. It lives in code, is owned by engineering, and changes only on deploy. This is the allowlist and the security authority.
-
Marketing fills and tests it, within bounds.
The platform generates the editing UI from the same definitions: a length-capped text input, a dropdown for an enum, a bounded number field. Values are runtime data, validated against the in-code schema. No deploy.
-
The app renders the resolved data, with its own components.
A variant is assigned deterministically and resolved into a concrete value map. Your React components consume that map. Apertio never touches the DOM. Every possible output is enumerable in advance.
// Types flow from the slot definitions. Full autocomplete.
const { useSlot } = createApertio(slots)
function Hero() {
const headline = useSlot('home.hero.headline')
const layout = useSlot('home.hero.layout')
// Your markup. Apertio supplies values, never pixels.
return layout === 'split'
? <SplitHero title={headline} />
: <CenteredHero title={headline} />
} Zero flicker
The variant is already in the HTML the server sends.
Variants resolve server-side or at the edge, so the assigned variant is in the server HTML on first paint. There is no default-then-swap, and no anti-flicker snippet hiding the whole page while a script decides what to show.
The old way
A client script loads, hides the page, reads the assignment, mutates the DOM, then reveals. The visitor sees a blank or a flash. The work happens after render, in the browser.
render → flash → swap
Apertio
Assignment is a deterministic hash, computed once and kept in a cookie. Resolution is a pure function of assignment and config. The value is in the markup the server sends. Nothing flickers because nothing is patched.
resolved → in server HTML → first paint
Security-first
Apertio installs without loosening your CSP.
A variant payload is a primitive or an enum key. Never markup, never an arbitrary URL, never a CSS or JS string. Apertio never injects third-party scripts and never touches the DOM, so a strict nonce-based CSP stays intact with Apertio installed.
Data, not code
The variant carries only a value or a key. Anything that maps to code is an enum key resolved against an engineer-defined registry. The data never carries the code.
Bounded by construction
Every possible rendered output is enumerable in advance, because variant data is constrained to an engineer-approved allowlist. Auditing is finite.
The deployed code is the authority
The schema embedded in the running app is the security boundary. The platform is an editing mirror. It can never make the app render out of bounds.
For every team
What changes for each team.
- Engineering
- Stop being the bottleneck for every test. Carry the work once per surface, not once per experiment.
- Marketing
- Own experiments end to end inside the slots engineering defined. Edit copy, swap options, set the split, with no ticket.
- Security
- No injected JS, no eval, a strict nonce-based CSP intact. Every rendered output is provably bounded to an approved allowlist.
The honest limitation
Only modeled slots are testable.
Novel surfaces still need engineering. We do not over-promise removing engineering from experimentation. What Apertio changes is when it is spent.
The mitigation is making slot definition and the publish loop so cheap that adding a surface is a five-minute pull request. Suite vendors over-promise here. Stating the limit plainly is the honest version, and it is why the bounds hold.
Open source
The schema lives in your code, and the source is MIT-licensed.
The schema lives in your code. The SDK is layered into @apertio/core, @apertio/react, and @apertio/next. Self-host is on the roadmap. The repo link is the proof: you can read exactly what renders and exactly what cannot.
# add the core SDK
pnpm add @apertio/core Marketing runs the tests. Engineering keeps the CSP.
Instrument typed slots once. Marketing tests within those bounds with no new code, and your CSP never moves.