@variantlab/core 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1209 -39
- package/docs/API.md +692 -0
- package/docs/ARCHITECTURE.md +430 -0
- package/docs/CONTRIBUTING.md +264 -0
- package/docs/ROADMAP.md +292 -0
- package/docs/SECURITY.md +323 -0
- package/docs/design/api-philosophy.md +347 -0
- package/docs/design/config-format.md +442 -0
- package/docs/design/design-principles.md +212 -0
- package/docs/design/targeting-dsl.md +433 -0
- package/docs/features/codegen.md +351 -0
- package/docs/features/crash-rollback.md +399 -0
- package/docs/features/debug-overlay.md +328 -0
- package/docs/features/hmac-signing.md +330 -0
- package/docs/features/killer-features.md +308 -0
- package/docs/features/multivariate.md +339 -0
- package/docs/features/qr-sharing.md +372 -0
- package/docs/features/targeting.md +481 -0
- package/docs/features/time-travel.md +306 -0
- package/docs/features/value-experiments.md +487 -0
- package/docs/phases/phase-2-expansion.md +307 -0
- package/docs/phases/phase-3-ecosystem.md +289 -0
- package/docs/phases/phase-4-advanced.md +306 -0
- package/docs/phases/phase-5-v1-stable.md +350 -0
- package/docs/research/bundle-size-analysis.md +279 -0
- package/docs/research/competitors.md +327 -0
- package/docs/research/framework-ssr-quirks.md +394 -0
- package/docs/research/naming-rationale.md +238 -0
- package/docs/research/origin-story.md +179 -0
- package/docs/research/security-threats.md +312 -0
- package/package.json +2 -1
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# Framework SSR quirks
|
|
2
|
+
|
|
3
|
+
Notes on how each target framework handles server-side rendering, hydration, and the implications for variantlab. This document is the reference for implementing each adapter.
|
|
4
|
+
|
|
5
|
+
## Table of contents
|
|
6
|
+
|
|
7
|
+
- [Common concerns](#common-concerns)
|
|
8
|
+
- [Next.js App Router](#nextjs-app-router)
|
|
9
|
+
- [Next.js Pages Router](#nextjs-pages-router)
|
|
10
|
+
- [Remix](#remix)
|
|
11
|
+
- [SvelteKit](#sveltekit)
|
|
12
|
+
- [SolidStart](#solidstart)
|
|
13
|
+
- [Nuxt](#nuxt)
|
|
14
|
+
- [Astro](#astro)
|
|
15
|
+
- [React Native](#react-native)
|
|
16
|
+
- [Edge runtime targets](#edge-runtime-targets)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Common concerns
|
|
21
|
+
|
|
22
|
+
Every SSR framework has the same core problem for A/B testing: the server renders one variant, the client hydrates, and if the client picks a different variant, React/Vue/Svelte throws a hydration mismatch error.
|
|
23
|
+
|
|
24
|
+
### Solutions available
|
|
25
|
+
|
|
26
|
+
1. **Sticky cookie**: server sets a cookie with the variant assignment; client reads the same cookie on hydration.
|
|
27
|
+
2. **Streaming with suspense**: server holds rendering until it knows the variant; client streams the result.
|
|
28
|
+
3. **Defer to client**: render a placeholder on server, pick the variant on client. This is the easy way but causes layout shift.
|
|
29
|
+
4. **Edge middleware**: assign the variant at the edge before the request hits the renderer, then pass it via headers.
|
|
30
|
+
|
|
31
|
+
variantlab supports all four, with sticky cookie as the recommended default.
|
|
32
|
+
|
|
33
|
+
### Invariants variantlab must uphold
|
|
34
|
+
|
|
35
|
+
- **Deterministic assignment**: given the same `(userId, experimentId, context)`, always produce the same variant.
|
|
36
|
+
- **No `Math.random()` on the hot path**: non-deterministic → hydration mismatches.
|
|
37
|
+
- **No `Date.now()` in pure assignment**: time-based targeting only at evaluation boundaries.
|
|
38
|
+
- **Stable ordering of experiments**: iteration order must match server and client.
|
|
39
|
+
- **No environment-specific branches in assignment logic**: server code must match client code byte-for-byte.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Next.js App Router
|
|
44
|
+
|
|
45
|
+
Next.js 14/15 App Router uses React Server Components (RSC). Server components can read variants freely; client components need a context.
|
|
46
|
+
|
|
47
|
+
### Architecture
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
┌─────────────────────────────────────────────────┐
|
|
51
|
+
│ middleware.ts │
|
|
52
|
+
│ Reads cookies, sets sticky variant cookie │
|
|
53
|
+
└──────────────┬──────────────────────────────────┘
|
|
54
|
+
│
|
|
55
|
+
▼
|
|
56
|
+
┌─────────────────────────────────────────────────┐
|
|
57
|
+
│ app/layout.tsx (Server Component) │
|
|
58
|
+
│ Reads cookies via next/headers │
|
|
59
|
+
│ Creates initial variant map │
|
|
60
|
+
│ Wraps children in VariantLabProvider │
|
|
61
|
+
│ (client component, re-hydrates from initial map)│
|
|
62
|
+
└──────────────┬──────────────────────────────────┘
|
|
63
|
+
│
|
|
64
|
+
▼
|
|
65
|
+
┌─────────────────────────────────────────────────┐
|
|
66
|
+
│ Server components: read via getVariantSSR() │
|
|
67
|
+
│ Client components: useVariant() hook │
|
|
68
|
+
└──────────────────────────────────────────────────┘
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Our implementation
|
|
72
|
+
|
|
73
|
+
- **Middleware** sets a cookie `variantlab_v1` containing an encoded map of `(experimentId → variantId)` for eligible experiments
|
|
74
|
+
- **Root layout** reads the cookie via `cookies()` from `next/headers`, decodes it, and passes it to `<VariantLabProvider initialVariants={...}>`
|
|
75
|
+
- **Server components** import `getVariantSSR(experimentId, cookies())` which is a pure function
|
|
76
|
+
- **Client components** use the React hooks via the provider
|
|
77
|
+
|
|
78
|
+
### Pitfalls
|
|
79
|
+
|
|
80
|
+
1. **Dynamic vs static rendering.** If a page reads cookies, Next.js marks it as dynamic. This affects caching. We document this clearly.
|
|
81
|
+
2. **RSC boundary.** `VariantLabProvider` must be a client component. We re-export it from `@variantlab/next/client` with `"use client"` directive.
|
|
82
|
+
3. **Cookie size.** Next.js cookies are limited to 4 KB. If you have many experiments, we compress the cookie (base64-encoded minified JSON, or pack variant IDs as small integers).
|
|
83
|
+
4. **Parallel routes and intercepting routes.** These can render the same provider multiple times. Our provider detects this and de-duplicates.
|
|
84
|
+
|
|
85
|
+
### Example
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
// middleware.ts
|
|
89
|
+
import { variantLabMiddleware } from "@variantlab/next/middleware";
|
|
90
|
+
import config from "./experiments.json";
|
|
91
|
+
|
|
92
|
+
export default variantLabMiddleware(config);
|
|
93
|
+
export const config = { matcher: ["/((?!_next|api).*)"] };
|
|
94
|
+
|
|
95
|
+
// app/layout.tsx
|
|
96
|
+
import { cookies } from "next/headers";
|
|
97
|
+
import { VariantLabProvider } from "@variantlab/next/client";
|
|
98
|
+
import experiments from "./experiments.json";
|
|
99
|
+
|
|
100
|
+
export default function RootLayout({ children }) {
|
|
101
|
+
const cookieStore = cookies();
|
|
102
|
+
const initialVariants = JSON.parse(
|
|
103
|
+
cookieStore.get("variantlab_v1")?.value ?? "{}"
|
|
104
|
+
);
|
|
105
|
+
return (
|
|
106
|
+
<html>
|
|
107
|
+
<body>
|
|
108
|
+
<VariantLabProvider
|
|
109
|
+
config={experiments}
|
|
110
|
+
initialVariants={initialVariants}
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
</VariantLabProvider>
|
|
114
|
+
</body>
|
|
115
|
+
</html>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// app/page.tsx (server component)
|
|
120
|
+
import { getVariantSSR } from "@variantlab/next";
|
|
121
|
+
import experiments from "./experiments.json";
|
|
122
|
+
|
|
123
|
+
export default async function Page() {
|
|
124
|
+
const cookieStore = cookies();
|
|
125
|
+
const variant = getVariantSSR("cta-copy", cookieStore, experiments);
|
|
126
|
+
return <button>{variant === "buy-now" ? "Buy now" : "Get started"}</button>;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Next.js Pages Router
|
|
133
|
+
|
|
134
|
+
Older model. Simpler but less flexible.
|
|
135
|
+
|
|
136
|
+
### Architecture
|
|
137
|
+
|
|
138
|
+
- `getServerSideProps` reads cookies, picks variant, passes as prop
|
|
139
|
+
- `_app.tsx` wraps in `<VariantLabProvider>` with initial variants
|
|
140
|
+
- Client hooks work as in any React app
|
|
141
|
+
|
|
142
|
+
### Example
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// pages/_app.tsx
|
|
146
|
+
import { VariantLabProvider } from "@variantlab/next/client";
|
|
147
|
+
import experiments from "../experiments.json";
|
|
148
|
+
|
|
149
|
+
export default function MyApp({ Component, pageProps }) {
|
|
150
|
+
return (
|
|
151
|
+
<VariantLabProvider
|
|
152
|
+
config={experiments}
|
|
153
|
+
initialVariants={pageProps.variantlab ?? {}}
|
|
154
|
+
>
|
|
155
|
+
<Component {...pageProps} />
|
|
156
|
+
</VariantLabProvider>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// pages/index.tsx
|
|
161
|
+
import { getVariantSSR } from "@variantlab/next";
|
|
162
|
+
import experiments from "../experiments.json";
|
|
163
|
+
|
|
164
|
+
export async function getServerSideProps({ req, res }) {
|
|
165
|
+
const variant = getVariantSSR("cta-copy", req, experiments);
|
|
166
|
+
return {
|
|
167
|
+
props: {
|
|
168
|
+
variantlab: { "cta-copy": variant },
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Pitfalls
|
|
175
|
+
|
|
176
|
+
- `getStaticProps` cannot run variant assignment (no cookies). Users must use `getServerSideProps` or client-side assignment.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Remix
|
|
181
|
+
|
|
182
|
+
Remix uses loaders for server-side data fetching. Loaders run on every request, making them ideal for variant assignment.
|
|
183
|
+
|
|
184
|
+
### Architecture
|
|
185
|
+
|
|
186
|
+
- A root loader reads cookies, creates an initial variant map, returns it
|
|
187
|
+
- `<VariantLabProvider>` wraps the root
|
|
188
|
+
- Action functions can also read variants
|
|
189
|
+
|
|
190
|
+
### Example
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
// app/root.tsx
|
|
194
|
+
import { json } from "@remix-run/node";
|
|
195
|
+
import { useLoaderData } from "@remix-run/react";
|
|
196
|
+
import { VariantLabProvider } from "@variantlab/remix";
|
|
197
|
+
import experiments from "./experiments.json";
|
|
198
|
+
import { parseVariantCookie } from "@variantlab/remix/server";
|
|
199
|
+
|
|
200
|
+
export async function loader({ request }) {
|
|
201
|
+
const cookieHeader = request.headers.get("Cookie") ?? "";
|
|
202
|
+
const initialVariants = parseVariantCookie(cookieHeader);
|
|
203
|
+
return json({ initialVariants });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export default function App() {
|
|
207
|
+
const { initialVariants } = useLoaderData<typeof loader>();
|
|
208
|
+
return (
|
|
209
|
+
<VariantLabProvider config={experiments} initialVariants={initialVariants}>
|
|
210
|
+
<Outlet />
|
|
211
|
+
</VariantLabProvider>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Pitfalls
|
|
217
|
+
|
|
218
|
+
- Remix nested routes mean the provider sits at the root. Inner routes cannot create their own provider without duplicating state.
|
|
219
|
+
- Action functions that redirect must re-set the variant cookie, otherwise the next loader will re-assign.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## SvelteKit
|
|
224
|
+
|
|
225
|
+
SvelteKit uses `load` functions (server and client) and hooks.
|
|
226
|
+
|
|
227
|
+
### Architecture
|
|
228
|
+
|
|
229
|
+
- `src/hooks.server.ts` uses `handle` to read cookies and set a local variant context
|
|
230
|
+
- `src/routes/+layout.server.ts` exposes the variant map via `event.locals.variantlab`
|
|
231
|
+
- `src/routes/+layout.svelte` wraps children in `<VariantLabProvider>`
|
|
232
|
+
|
|
233
|
+
### Example
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
// src/hooks.server.ts
|
|
237
|
+
import type { Handle } from "@sveltejs/kit";
|
|
238
|
+
import { createEngine } from "@variantlab/core";
|
|
239
|
+
import experiments from "../experiments.json";
|
|
240
|
+
|
|
241
|
+
export const handle: Handle = async ({ event, resolve }) => {
|
|
242
|
+
const engine = createEngine(experiments, { storage: event.cookies });
|
|
243
|
+
event.locals.variantlab = engine;
|
|
244
|
+
return resolve(event);
|
|
245
|
+
};
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```svelte
|
|
249
|
+
<!-- src/routes/+layout.svelte -->
|
|
250
|
+
<script>
|
|
251
|
+
import { VariantLabProvider } from "@variantlab/svelte";
|
|
252
|
+
import experiments from "../experiments.json";
|
|
253
|
+
export let data;
|
|
254
|
+
</script>
|
|
255
|
+
|
|
256
|
+
<VariantLabProvider config={experiments} initialVariants={data.variantlab}>
|
|
257
|
+
<slot />
|
|
258
|
+
</VariantLabProvider>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Pitfalls
|
|
262
|
+
|
|
263
|
+
- Svelte 4 vs Svelte 5 have different runes. We support both with separate entry points.
|
|
264
|
+
- `$app/stores` vs `$app/state` (Svelte 5) — we abstract over both.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## SolidStart
|
|
269
|
+
|
|
270
|
+
Solid is similar to React but uses fine-grained reactivity via signals.
|
|
271
|
+
|
|
272
|
+
### Architecture
|
|
273
|
+
|
|
274
|
+
- `createVariantLabEngine` returns a signal-based store
|
|
275
|
+
- `useVariant` returns an accessor function (not a value directly)
|
|
276
|
+
- `<VariantLabProvider>` is a regular component
|
|
277
|
+
|
|
278
|
+
### Pitfalls
|
|
279
|
+
|
|
280
|
+
- Solid's reactivity graph is different from React's. We must not use React's `useSyncExternalStore`-style pattern; instead we create signals.
|
|
281
|
+
- SSR in SolidStart uses `createAsync` and `isServer` checks. We handle both.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Nuxt
|
|
286
|
+
|
|
287
|
+
Vue 3 meta-framework. Uses Nitro server routes and the Vue composition API.
|
|
288
|
+
|
|
289
|
+
### Architecture
|
|
290
|
+
|
|
291
|
+
- `nuxt.config.ts` installs `@variantlab/nuxt` module
|
|
292
|
+
- Module auto-imports `useVariant()` composable
|
|
293
|
+
- Server-side: `event.context.variantlab` via Nitro middleware
|
|
294
|
+
- Client-side: `<VariantLabProvider>` via plugin
|
|
295
|
+
|
|
296
|
+
### Pitfalls
|
|
297
|
+
|
|
298
|
+
- Nuxt's auto-import can cause name collisions. We prefix our composables with `useVariant*` to minimize conflict.
|
|
299
|
+
- Server and client must share the same engine state via `useState`-style hydration.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Astro
|
|
304
|
+
|
|
305
|
+
Astro uses "islands" — interactive components inside a mostly-static HTML page. Different framework islands can coexist on the same page.
|
|
306
|
+
|
|
307
|
+
### Architecture
|
|
308
|
+
|
|
309
|
+
- `astro.config.mjs` installs `@variantlab/astro` integration
|
|
310
|
+
- Pages and components read variants via `Astro.locals.variantlab` at build/request time
|
|
311
|
+
- Islands hydrate with initial variants passed as props
|
|
312
|
+
|
|
313
|
+
### Pitfalls
|
|
314
|
+
|
|
315
|
+
- Each island is its own framework instance (React island, Vue island, etc.). Each needs its own provider hydration.
|
|
316
|
+
- SSG vs SSR: for static pages, variant selection happens at build time or at the edge middleware layer.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## React Native
|
|
321
|
+
|
|
322
|
+
No SSR. Simpler, but has its own quirks.
|
|
323
|
+
|
|
324
|
+
### Architecture
|
|
325
|
+
|
|
326
|
+
- `<VariantLabProvider>` wraps the app
|
|
327
|
+
- Storage uses AsyncStorage / MMKV / SecureStore
|
|
328
|
+
- Debug overlay uses RN native components
|
|
329
|
+
- Deep link handler integrates with Expo Linking or React Navigation
|
|
330
|
+
|
|
331
|
+
### Pitfalls
|
|
332
|
+
|
|
333
|
+
1. **Hermes optimization**: Some JS features that work in V8 don't optimize well in Hermes. We avoid `Proxy` in hot paths.
|
|
334
|
+
2. **New Architecture (Fabric) compatibility**: We test against both Old and New architectures.
|
|
335
|
+
3. **Offline**: storage reads can fail. We fall back to in-memory defaults.
|
|
336
|
+
4. **AsyncStorage vs MMKV**: AsyncStorage is async, MMKV is sync. We handle both in the `Storage` interface.
|
|
337
|
+
5. **Expo Go vs bare**: some APIs (SecureStore) are available only in custom dev builds. We document this clearly.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Edge runtime targets
|
|
342
|
+
|
|
343
|
+
### Cloudflare Workers
|
|
344
|
+
|
|
345
|
+
- Web API subset (no Node built-ins)
|
|
346
|
+
- `crypto.subtle` available — our HMAC verification works out of the box
|
|
347
|
+
- KV storage for remote config caching
|
|
348
|
+
- 1 MB unzipped bundle limit — our 3 KB target is well within
|
|
349
|
+
|
|
350
|
+
### Vercel Edge
|
|
351
|
+
|
|
352
|
+
- Similar to Cloudflare Workers
|
|
353
|
+
- `NextRequest` / `NextResponse` APIs
|
|
354
|
+
- Middleware runs at the edge — perfect for cookie-based assignment
|
|
355
|
+
|
|
356
|
+
### Deno Deploy
|
|
357
|
+
|
|
358
|
+
- Full Deno runtime
|
|
359
|
+
- `crypto.subtle` available
|
|
360
|
+
- Standard Web Fetch API
|
|
361
|
+
|
|
362
|
+
### Bun
|
|
363
|
+
|
|
364
|
+
- Mostly Node-compatible
|
|
365
|
+
- Bun's fast `Bun.hash` can optionally be used for sticky assignment (gated behind a check)
|
|
366
|
+
|
|
367
|
+
### AWS Lambda@Edge
|
|
368
|
+
|
|
369
|
+
- Node-based but runs at CloudFront edge
|
|
370
|
+
- Limited execution time (low milliseconds)
|
|
371
|
+
- We fit within the constraints
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Implementation priority
|
|
376
|
+
|
|
377
|
+
Based on framework popularity and user demand, we'll implement SSR support in this order:
|
|
378
|
+
|
|
379
|
+
1. **Next.js App Router** (Phase 1) — largest audience
|
|
380
|
+
2. **Next.js Pages Router** (Phase 1) — still widely used
|
|
381
|
+
3. **React Native** (Phase 1) — primary audience for our debug overlay killer feature
|
|
382
|
+
4. **Remix** (Phase 2)
|
|
383
|
+
5. **SvelteKit** (Phase 3)
|
|
384
|
+
6. **SolidStart** (Phase 3)
|
|
385
|
+
7. **Nuxt** (Phase 3)
|
|
386
|
+
8. **Astro** (Phase 3)
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## See also
|
|
391
|
+
|
|
392
|
+
- [`API.md`](../../API.md#variantlabnext) — the SSR API surface
|
|
393
|
+
- [`docs/features/hmac-signing.md`](../features/hmac-signing.md) — Web Crypto API usage
|
|
394
|
+
- [`ARCHITECTURE.md`](../../ARCHITECTURE.md#runtime-data-flow) — engine data flow
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Naming rationale
|
|
2
|
+
|
|
3
|
+
Why `variantlab`, what we considered, and what still needs validation.
|
|
4
|
+
|
|
5
|
+
## Table of contents
|
|
6
|
+
|
|
7
|
+
- [Current pick](#current-pick)
|
|
8
|
+
- [Requirements](#requirements)
|
|
9
|
+
- [Candidates considered](#candidates-considered)
|
|
10
|
+
- [Why variantlab won](#why-variantlab-won)
|
|
11
|
+
- [Risks](#risks)
|
|
12
|
+
- [Final validation checklist](#final-validation-checklist)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Current pick
|
|
17
|
+
|
|
18
|
+
**`variantlab`** — npm scope `@variantlab/*`, domain `variantlab.dev`.
|
|
19
|
+
|
|
20
|
+
The name captures three things:
|
|
21
|
+
|
|
22
|
+
1. **Variant** — the unit of work. Not "flags", not "experiments", not "tests". A variant is a thing a user sees.
|
|
23
|
+
2. **Lab** — a place for controlled experimentation. Evokes the scientific rigor we want to bring to UX.
|
|
24
|
+
3. **Neutral** — works for feature flags, A/B tests, gradual rollouts, and UX experiments alike.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
Before deciding, we listed what the name had to support:
|
|
31
|
+
|
|
32
|
+
1. **Short enough to type** — under 12 characters ideally
|
|
33
|
+
2. **Available on npm** — the `@name/*` scope must be free
|
|
34
|
+
3. **Available domain** — `.dev` or `.io` at minimum
|
|
35
|
+
4. **Available on GitHub** — organization name free
|
|
36
|
+
5. **Pronounceable** — non-native English speakers can say it
|
|
37
|
+
6. **Not a trademark** — no existing company with a clashing name
|
|
38
|
+
7. **Memorable** — sticks in the head
|
|
39
|
+
8. **Searchable** — has unique search results, not drowned by a common word
|
|
40
|
+
9. **Extensible** — `@name/core`, `@name/react`, etc. all read well
|
|
41
|
+
10. **Neutral connotation** — no religion, no politics, no culture-specific meanings
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Candidates considered
|
|
46
|
+
|
|
47
|
+
We went through ~30 names. Here are the serious contenders:
|
|
48
|
+
|
|
49
|
+
### `variantlab`
|
|
50
|
+
|
|
51
|
+
**Pros**: Captures the concept, neutral, scientific, 10 chars, `@variantlab/react` reads well.
|
|
52
|
+
**Cons**: Slightly generic, may be confused with lab equipment brands.
|
|
53
|
+
**npm `@variantlab` scope**: needs to be claimed.
|
|
54
|
+
**variantlab.dev**: needs to be registered.
|
|
55
|
+
|
|
56
|
+
### `flagkit`
|
|
57
|
+
|
|
58
|
+
**Pros**: Very short (7 chars), memorable, "kit" implies bundled tools.
|
|
59
|
+
**Cons**: "flag" has political/territorial connotations that could be misread. Narrows the concept to feature flags when we want to cover A/B testing, gradual rollout, and UI experiments.
|
|
60
|
+
**Verdict**: Too narrow.
|
|
61
|
+
|
|
62
|
+
### `shift`
|
|
63
|
+
|
|
64
|
+
**Pros**: 5 chars, bold, evokes "shifting variants".
|
|
65
|
+
**Cons**: Almost certainly taken on npm. Overloaded meaning (keyboard shift, shift left, etc.).
|
|
66
|
+
**Verdict**: Too common.
|
|
67
|
+
|
|
68
|
+
### `omniflag`
|
|
69
|
+
|
|
70
|
+
**Pros**: "Omni" captures universal framework support.
|
|
71
|
+
**Cons**: 8 chars + "flag" issue. Sounds like an enterprise brand.
|
|
72
|
+
**Verdict**: Too corporate.
|
|
73
|
+
|
|
74
|
+
### `polyvariant`
|
|
75
|
+
|
|
76
|
+
**Pros**: Scientific, captures multi-variant testing, unique.
|
|
77
|
+
**Cons**: 11 chars + hard to spell + "poly" sounds prefix-y.
|
|
78
|
+
**Verdict**: Too academic.
|
|
79
|
+
|
|
80
|
+
### `labkit`
|
|
81
|
+
|
|
82
|
+
**Pros**: Short, evokes tooling.
|
|
83
|
+
**Cons**: Almost certainly taken (common laboratory supply brand).
|
|
84
|
+
**Verdict**: Trademark risk.
|
|
85
|
+
|
|
86
|
+
### `flaglab`
|
|
87
|
+
|
|
88
|
+
**Pros**: Combines "flag" and "lab".
|
|
89
|
+
**Cons**: Narrow (see flag issue), and npm scope likely taken.
|
|
90
|
+
**Verdict**: Ruled out.
|
|
91
|
+
|
|
92
|
+
### `expt`
|
|
93
|
+
|
|
94
|
+
**Pros**: 4 chars, techy.
|
|
95
|
+
**Cons**: Looks like a typo. Hard to pronounce.
|
|
96
|
+
**Verdict**: Too cute.
|
|
97
|
+
|
|
98
|
+
### `variox`
|
|
99
|
+
|
|
100
|
+
**Pros**: Unique, 6 chars, no existing trademark.
|
|
101
|
+
**Cons**: Invented word, not immediately meaningful.
|
|
102
|
+
**Verdict**: Backup option.
|
|
103
|
+
|
|
104
|
+
### `hypothesize`
|
|
105
|
+
|
|
106
|
+
**Pros**: Scientific, unique.
|
|
107
|
+
**Cons**: 11 chars, hard to type repeatedly.
|
|
108
|
+
**Verdict**: Too long.
|
|
109
|
+
|
|
110
|
+
### `tryout`
|
|
111
|
+
|
|
112
|
+
**Pros**: Short, friendly, captures "trying variants".
|
|
113
|
+
**Cons**: Lacks technical gravitas, probably taken.
|
|
114
|
+
**Verdict**: Too casual.
|
|
115
|
+
|
|
116
|
+
### `abx`
|
|
117
|
+
|
|
118
|
+
**Pros**: 3 chars, technical.
|
|
119
|
+
**Cons**: Mysterious, not memorable, possibly medical connotation (antibiotic).
|
|
120
|
+
**Verdict**: Too cryptic.
|
|
121
|
+
|
|
122
|
+
### `testlab`
|
|
123
|
+
|
|
124
|
+
**Pros**: Self-explanatory.
|
|
125
|
+
**Cons**: Generic, "test" is overloaded with unit testing. Probably taken.
|
|
126
|
+
**Verdict**: Too generic.
|
|
127
|
+
|
|
128
|
+
### `switchboard`
|
|
129
|
+
|
|
130
|
+
**Pros**: Evokes a control panel.
|
|
131
|
+
**Cons**: 11 chars, dated metaphor.
|
|
132
|
+
**Verdict**: Too long.
|
|
133
|
+
|
|
134
|
+
### `stageset`
|
|
135
|
+
|
|
136
|
+
**Pros**: Evokes staging variants.
|
|
137
|
+
**Cons**: Unclear meaning, 8 chars.
|
|
138
|
+
**Verdict**: Confusing.
|
|
139
|
+
|
|
140
|
+
### `exposure`
|
|
141
|
+
|
|
142
|
+
**Pros**: Technical term from experimentation (exposure events).
|
|
143
|
+
**Cons**: 8 chars, also has negative connotations (exposure to risk, exposure of data).
|
|
144
|
+
**Verdict**: Mixed signal.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Why variantlab won
|
|
149
|
+
|
|
150
|
+
Looking at the shortlist:
|
|
151
|
+
|
|
152
|
+
| Name | Length | Meaningful | Neutral | Extensible | Available? |
|
|
153
|
+
|---|---:|:-:|:-:|:-:|:-:|
|
|
154
|
+
| variantlab | 10 | ✅ | ✅ | ✅ | TBD |
|
|
155
|
+
| polyvariant | 11 | ✅ | ✅ | ✅ | Likely |
|
|
156
|
+
| variox | 6 | Partial | ✅ | ✅ | Likely |
|
|
157
|
+
| flagkit | 7 | Narrow | Neutral | ✅ | TBD |
|
|
158
|
+
| shift | 5 | Partial | ✅ | ❌ | Unlikely |
|
|
159
|
+
|
|
160
|
+
`variantlab` edges out `polyvariant` on pronounceability and `variox` on meaning. It beats `flagkit` by not pigeonholing us into feature flags.
|
|
161
|
+
|
|
162
|
+
The package-family reads nicely:
|
|
163
|
+
|
|
164
|
+
- `@variantlab/core`
|
|
165
|
+
- `@variantlab/react`
|
|
166
|
+
- `@variantlab/react-native`
|
|
167
|
+
- `@variantlab/next`
|
|
168
|
+
- `@variantlab/cli`
|
|
169
|
+
- `@variantlab/devtools`
|
|
170
|
+
|
|
171
|
+
The tagline writes itself: *"Every framework. Zero lock-in. One lab."*
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Risks
|
|
176
|
+
|
|
177
|
+
### Risk 1 — npm scope taken
|
|
178
|
+
|
|
179
|
+
We need to verify `@variantlab` is free on npm before committing. If it's taken but inactive, we might be able to request it via npm support. If actively used, we need a backup.
|
|
180
|
+
|
|
181
|
+
**Backup plan**: If `@variantlab` is unavailable, fall back to `@variox/*` or `@lab-variant/*`.
|
|
182
|
+
|
|
183
|
+
### Risk 2 — trademark conflict
|
|
184
|
+
|
|
185
|
+
"Variant Lab" may exist as a company name in biotech or marketing. We need a trademark check before launching publicly.
|
|
186
|
+
|
|
187
|
+
**Backup plan**: Add a distinctive suffix if needed (`variantlab.dev` domain clarifies context).
|
|
188
|
+
|
|
189
|
+
### Risk 3 — SEO
|
|
190
|
+
|
|
191
|
+
"variant" and "lab" are both common words. Initial Google searches may be noisy. We plan to claim the name via:
|
|
192
|
+
|
|
193
|
+
- GitHub organization
|
|
194
|
+
- npm scope
|
|
195
|
+
- `.dev` domain
|
|
196
|
+
- Twitter / Mastodon / Bluesky handles
|
|
197
|
+
- Product Hunt launch
|
|
198
|
+
|
|
199
|
+
### Risk 4 — Misinterpretation
|
|
200
|
+
|
|
201
|
+
Some readers might assume "lab" means unstable or experimental (like Chrome flags). We mitigate by emphasizing "stable once shipped" in the tagline and README.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Final validation checklist
|
|
206
|
+
|
|
207
|
+
Before we commit to the name in code:
|
|
208
|
+
|
|
209
|
+
- [ ] Verify `@variantlab` is free on npm
|
|
210
|
+
- [ ] Register `variantlab.dev`
|
|
211
|
+
- [ ] Create GitHub organization `variantlab`
|
|
212
|
+
- [ ] Reserve handles on Twitter, Bluesky, Mastodon, LinkedIn
|
|
213
|
+
- [ ] Run a trademark search in US, EU, and India
|
|
214
|
+
- [ ] Socialize the name in a GitHub discussion for community feedback
|
|
215
|
+
- [ ] Verify no existing OSS project uses the name actively
|
|
216
|
+
|
|
217
|
+
If any of these fail, we fall back to the shortlist in order: `polyvariant` → `variox` → a new brainstorm.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Fallback: invent a word
|
|
222
|
+
|
|
223
|
+
If all pre-existing candidates fail, we invent one. Requirements for an invented name:
|
|
224
|
+
|
|
225
|
+
- 5-8 characters
|
|
226
|
+
- Pronounceable in English, Spanish, and Hindi
|
|
227
|
+
- No existing meaning in any major language (verified via Wiktionary)
|
|
228
|
+
- Clean trademark search
|
|
229
|
+
- `.dev` domain available
|
|
230
|
+
- Short npm scope available
|
|
231
|
+
|
|
232
|
+
Candidates if we go this route: `varex`, `expio`, `experi`, `splitly`, `sprou`.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Decision
|
|
237
|
+
|
|
238
|
+
**Go with `variantlab` pending the validation checklist.** If blocked, revisit this document.
|