Experiments
Deterministic A/B experimentation — sticky variant assignment and deduped exposure tracking, as a Bijection component
git clone https://github.com/bijectionhq/experiments.git
cd experiments
git checkout f9e797855da70dbc3572377a23fe1ac794fb0a32Bijection repository access is required. Request access.
Description
Deterministic A/B experimentation — sticky variant assignment and deduped exposure tracking, as a Bijection component
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
This package has a pinned source revision in Bijection’s workspace. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: bijectionhq/experiments. Based on vllnt/convex-experiments. License: (MIT AND Apache-2.0). Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
Deterministic A/B experimentation — sticky variant assignment and deduped exposure tracking, as a Convex component
Explore the demo ↗Upstream overview
From the upstream package documentation.
@vllnt/convex-experiments assigns subjects to weighted variants and records which variants they encounter. It supplies the assignment and exposure layer of an A/B test while your application measures outcomes.
Define weighted variants, start enrollment, and use logExposure to assign and record a subject. peek resolves a variant without writing, and results returns assignment and exposure tallies. Stopping an experiment preserves its recorded data. Scopes separate experiments across surfaces or tenants.
The host authenticates subjects, authorizes experiment management, and records conversions or quality scores. Exposure tallies are not conversion analysis or statistical significance. Use a new experiment key when changing an established allocation.
Maintained by bntvllnt at VLLNT. Find the author on GitHub and X.
Use cases
From the upstream package documentation.
- Mobile onboarding: keep each person on the same introduction flow across visits.
- SaaS conversion tests: compare checkout copy, upgrade screens, or feature presentations.
- AI applications: assign prompt or model configurations and record outcomes in your own evaluation data.
- Games and learning: compare challenge presentations or tutorial variants with stable enrollment.
How it works
From the upstream package documentation.
Define weighted variants, start enrollment, and use logExposure to assign and record a subject. peek resolves a variant without writing, and results returns assignment and exposure tallies. Stopping an experiment preserves its recorded data. Scopes separate experiments across surfaces or tenants.
The host authenticates subjects, authorizes experiment management, and records conversions or quality scores. Exposure tallies are not conversion analysis or statistical significance. Use a new experiment key when changing an established allocation.
Upstream package FAQ
Can the same subject ever be reassigned to a different variant after initial enrollment?
No. In @vllnt/convex-experiments, once a subject is assigned to a variant that assignment is immutable. The variants and salt fields are also locked after the first enrollment, and attempting to change them throws an error. To re-randomize a cohort you define a new experiment key.
How does the component handle GDPR right-to-erasure requests?
The @vllnt/convex-experiments component exposes a forgetSubject mutation that deletes a single subject's assignment and exposure records for a given experiment key and optional scope. The deleteExperiment mutation performs a full cascade delete of all data for an experiment. Neither operation affects the per-variant result tallies for other subjects.
Does the component include authentication or access control?
The @vllnt/convex-experiments component is auth-agnostic. The host application is responsible for resolving identity and deciding who may call define, setStatus, logExposure, or forgetSubject. The component receives an opaque subjectRef string and never inspects it, so you pass in whatever identifier your auth layer provides.
Is there a way to read a subject's variant in React without triggering a write?
Yes. The component exposes a peek query that returns the deterministic variant for a subject without writing any data. The optional @vllnt/convex-experiments/react package provides a useVariant hook that wraps a host-re-exported peek query reference, enabling flicker-free first paint. The React package is tree-shakeable and is not pulled in for backend-only consumers.
What Convex version is required, and how is the component registered?
The @vllnt/convex-experiments component requires convex@^1.41.0 as a peer dependency. It is registered by importing the convex.config entry point and calling app.use(experiments) inside your convex/convex.config.ts file, following the standard Convex component installation pattern.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Experiments with Bijection.
Read: https://bijection.com/components/experiments/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from vllnt/convex-experiments, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
@vllnt/convex-experiments
Deterministic A/B experimentation — sticky variant assignment and deduped exposure tracking, as a Convex component.
const experiments = new Experiments(components.experiments);
await experiments.define(ctx, "checkout_button", {
variants: [
{ key: "control", weight: 1 },
{ key: "treatment", weight: 1 },
],
});
const { variant } = await experiments.logExposure(ctx, "checkout_button", userId);
if (variant === "treatment") showNewButton();
Define an experiment with weighted variants; enroll subjects into a sticky variant chosen
deterministically from (salt, subjectRef); record exposures and read per-variant tallies. Pair it
with a feature-flag system for kill-switches and measure conversions in your own tables — this
component handles assignment and exposure. Domain-neutral: any subject, any surface.
Features
- Deterministic + sticky — the same subject always lands in the same weighted variant, even before anything is stored.
- Weighted variants — split traffic by relative weights (
1:1,90:10, N-way); equal weights split evenly. - Enrollment lifecycle —
draft → running → stopped; onlyrunningenrolls, and stopping preserves recorded data. - Deduped exposures + O(variants) results — one tallied row per subject;
resultsreads maintained per-variant tallies (assigned/subjects/exposures/weight), never scanning the exposure table. - Sample-ratio-ready —
assigned+weightper variant let you check observed vs expected split (SRM). peek— a read-only deterministic query returns a subject's sticky variant without writing (SSR / flicker-free first paint).- Immutable once assigned —
variants/saltare fixed after enrollment (changing them throws); define a new key to re-randomize. - Lifecycle + GDPR —
listExperimentsto discover,forgetSubjectto erase one subject,deleteExperimentto cascade-delete. - Scopes — global by default, or namespace per tenant / surface (folded into the hash for independent bucketing).
- Fully typed — variant keys, weights, and outcomes are concrete types end to end; no
any. - Server-sourced time — assignment timestamps come from the server, never the caller.
Installation
pnpm add @vllnt/convex-experiments
Peer dependency: convex@^1.41.0.
Usage
// convex/convex.config.ts
import { defineApp } from "convex/server";
import experiments from "@vllnt/convex-experiments/convex.config";
const app = defineApp();
app.use(experiments);
export default app;
// convex/checkout.ts — host owns auth; pass an opaque subjectRef in.
import { components } from "./_generated/api";
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { Experiments } from "@vllnt/convex-experiments";
const experiments = new Experiments(components.experiments);
export const view = mutation({
args: { userId: v.string() },
handler: async (ctx, { userId }) => {
const { variant } = await experiments.logExposure(ctx, "checkout_button", userId);
return { variant }; // "control" | "treatment" | null (not enrolled)
},
});
Measure the outcome in your own table, joined on the assigned variant — then read tallies with
results(ctx, "checkout_button"). See example/convex/example.ts for a
host-side conversion recorder.
API Reference
| Method | Kind | Result |
|---|---|---|
define(ctx, key, { variants, scope?, salt?, status? }) | mutation | { created: boolean } |
setStatus(ctx, key, status, scope?) | mutation | boolean |
assign(ctx, key, subjectRef, scope?) | mutation | { variant: null } | { variant: string; isNew: boolean } |
logExposure(ctx, key, subjectRef, scope?) | mutation | { variant: string | null } |
forgetSubject(ctx, key, subjectRef, scope?) | mutation | boolean (GDPR erasure) |
deleteExperiment(ctx, key, { scope?, batch? }) | mutation | number (cascade delete) |
getExperiment(ctx, key, scope?) | query | ExperimentDefinition | null |
listExperiments(ctx, { scope?, status? }) | query | ExperimentDefinition[] |
getAssignment(ctx, key, subjectRef, scope?) | query | { variant, assignedAt } | null |
peek(ctx, key, subjectRef, scope?) | query | { variant: string | null } (no write) |
results(ctx, key, scope?) | query | { variant, assigned, subjects, exposures, weight }[] |
Full reference: docs/API.md.
React
Optional, tree-shakeable hooks via @vllnt/convex-experiments/react (react is an optional peer
dep — a backend-only consumer pulls none of it). Each hook wraps useQuery over a query reference
you re-export from your app, so the component never owns your api.
// convex/experiments.ts — re-export the host-side wrappers (auth gated)
// export const myVariant = query({ args: { userId: v.string() },
// handler: (ctx, { userId }) => experiments.peek(ctx, "checkout_button", userId) });
import { useVariant } from "@vllnt/convex-experiments/react";
import { api } from "@/convex/_generated/api";
function CheckoutButton({ userId }: { userId: string }) {
// deterministic first paint via peek → no flash-of-control
const variant = useVariant(api.experiments.myVariant, { key: "checkout_button", subjectRef: userId });
return variant === "treatment" ? <OneClick /> : <Classic />;
}
| Hook | Wraps | Returns |
|---|---|---|
useVariant(peekRef, args) | peek | string | null | undefined (variant / not-enrolled / loading) |
useAssignment(getAssignmentRef, args) | getAssignment | Assignment | null | undefined |
useExperimentResults(resultsRef, args) | results | VariantResult[] | undefined |
Security
- Auth-agnostic — the host resolves identity and decides who may define, start, stop, or enroll.
- Tables sandboxed — reached only through the exported functions;
subjectRef, variant keys, andscopestay opaque. - Server-sourced timestamps — a caller cannot supply assignment time.
See docs/API.md.
Testing
pnpm test # single run
pnpm test:coverage # enforced 100% on covered files
Tests run against the real component runtime via convex-test (@edge-runtime/vm), not mocks.
Contributing
See CONTRIBUTING.md.
Author
Built by bntvllnt · bntvllnt.com · X @bntvllnt
Part of the @vllnt Convex component fleet — vllnt.com
If this is useful, sponsor the work.
License
MIT — see LICENSE.