Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

Experiments

Deterministic A/B experimentation — sticky variant assignment and deduped exposure tracking, as a Bijection component

Bijection source
git clone https://github.com/bijectionhq/experiments.git
cd experiments
git checkout f9e797855da70dbc3572377a23fe1ac794fb0a32

Bijection 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.

Included in Bijection’s source workspace

This package has a pinned source revision in Bijection’s workspace. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. 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.

agent-prompt.md
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; only running enrolls, and stopping preserves recorded data.
  • Deduped exposures + O(variants) results — one tallied row per subject; results reads maintained per-variant tallies (assigned/subjects/exposures/weight), never scanning the exposure table.
  • Sample-ratio-ready — assigned + weight per 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/salt are fixed after enrollment (changing them throws); define a new key to re-randomize.
  • Lifecycle + GDPR — listExperiments to discover, forgetSubject to erase one subject, deleteExperiment to 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

MethodKindResult
define(ctx, key, { variants, scope?, salt?, status? })mutation{ created: boolean }
setStatus(ctx, key, status, scope?)mutationboolean
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?)mutationboolean (GDPR erasure)
deleteExperiment(ctx, key, { scope?, batch? })mutationnumber (cascade delete)
getExperiment(ctx, key, scope?)queryExperimentDefinition | null
listExperiments(ctx, { scope?, status? })queryExperimentDefinition[]
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 />;
}
HookWrapsReturns
useVariant(peekRef, args)peekstring | null | undefined (variant / not-enrolled / loading)
useAssignment(getAssignmentRef, args)getAssignmentAssignment | null | undefined
useExperimentResults(resultsRef, args)resultsVariantResult[] | 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, and scope stay 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.

Keywords