nuxt-feature-flags
v2.0.5
Published
Feature flags for Nuxt with A/B testing and variant support
Maintainers
Readme
Nuxt Feature Flags
Type-safe feature flags for Nuxt 3 and Nuxt 4, with server-side runtime evaluation and built-in A/B testing.
Highlights
- Works with Nuxt 3.1+ and Nuxt 4 (same module, no separate build).
- Flags defined inline in
nuxt.config.ts, or in a separate config file — sync or async, plain object or a function evaluated per-request. - Deterministic, sticky A/B/n variant assignment (hashed by user id, session cookie, or IP — no external service needed), computed fresh per visitor on every request.
- SSR-safe: flags are resolved once per request on the server, then reused on the client without a second computation.
- Works across SSR, SPA, and static generation — see Rendering modes for per-mode support and caveats.
- Auto-imported composables, a
v-featuredirective, and a server-side helper.
Installation
npx nuxi module add nuxt-feature-flagsQuick Start
1. Enable the module
Either point it at a config file:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-feature-flags'],
featureFlags: {
config: './feature-flags.config.ts',
},
})...or declare flags inline, with no config file at all:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-feature-flags'],
featureFlags: {
flags: {
newDashboard: true,
checkoutExperiment: {
enabled: true,
value: 'control',
variants: [
{ name: 'control', weight: 50, value: 'control' },
{ name: 'treatment', weight: 50, value: 'treatment' },
],
},
},
},
})2. Define flags in a config file (optional)
// feature-flags.config.ts
import { defineFeatureFlags } from '#feature-flags/handler'
export default defineFeatureFlags((context) => {
return {
// `context` is the request's H3Event context — whatever your server
// middleware attaches to it (auth user, tenant, headers, KV bindings...).
isAdmin: context?.user?.role === 'admin',
newDashboard: true,
checkoutExperiment: {
enabled: true,
value: 'control',
variants: [
{ name: 'control', weight: 50, value: 'control' },
{ name: 'treatment', weight: 50, value: 'treatment' },
],
},
}
})A function-shaped config is evaluated on the server for every request (subject to cacheTTL), with the real H3Event context — it is never invoked at build time. This makes it safe to read cookies, headers, auth state, or bindings only available at request time (e.g. Cloudflare KV). Async functions are also supported — await a remote source and return the flags.
3. Use flags on the client
<script setup lang="ts">
const { flags, isEnabled, getValue, getVariant } = useFeatureFlags()
const { flags: asyncFlags, pending, error, refresh } = useAsyncFeatureFlags()
</script>
<template>
<NewDashboard v-if="isEnabled('newDashboard')" />
<div v-feature="'newDashboard'">
Also hidden/removed from the DOM when the flag is off.
</div>
<div v-if="pending">Loading flags...</div>
<div v-else-if="error">Could not refresh flags</div>
<CheckoutV2 v-else-if="asyncFlags.checkoutExperiment?.enabled" />
</template>useFeatureFlags()reads the flags already resolved during SSR (no extra request).useAsyncFeatureFlags()additionally re-fetches from the server on demand viarefresh().
4. Use flags on the server
// server/api/data.ts
// `getFeatureFlags` is auto-imported in server/ code — no import needed.
export default defineEventHandler(async (event) => {
const { isEnabled, getVariant } = await getFeatureFlags(event)
if (!isEnabled('newDashboard')) {
throw createError({ statusCode: 404, statusMessage: 'Feature disabled' })
}
return {
variant: getVariant('checkoutExperiment'),
}
})Outside of server/ auto-imports (e.g. a Nuxt plugin), import it explicitly:
import { getFeatureFlags } from '#feature-flags/server/utils'How variants are assigned
For a flag with variants, each request is bucketed deterministically: the module hashes flagName + identifier (SHA-256), where identifier is the first available of event.context.user.id, a session cookie (session_id / session-id / nuxt-session), or the request IP. The same visitor always gets the same variant for a given flag, without any external experimentation service.
Rendering modes
| Mode | Status | Notes |
|---|---|---|
| SSR (ssr: true, default) | ✅ Works | Flags resolved per request on the server with the real H3Event context, then hydrated on the client. |
| SPA (ssr: false, deployed with a live Nitro server) | ✅ Works | The .server plugin correctly no-ops (no SSR render happens in this mode); the client fetches flags from /api/_feature-flags/feature-flags on mount instead. |
| SPA on fully static hosting (ssr: false, no server function at all — e.g. GitHub Pages) | ⚠️ Not supported | The flags API route needs a live server. With no server in the output, that request 404s and flags stay unresolved. |
| Static generation (nuxi generate) | ⚠️ Works, with a caveat | Flags (including A/B variants) are computed once at build time and baked into the prerendered HTML for every visitor. There's no live request to differentiate visitors, so this is fine for static config-style flags, but it is not real per-visitor A/B testing — everyone who loads a given static build gets the same variant. |
| Hybrid rendering (routeRules with isr/swr/prerender) | ✅ Works | Routes that still render on the server go through the same code path as SSR. |
How server-side caching works
resolveFeatureFlags caches the flag definitions returned by your config (the same for every visitor) for up to cacheTTL ms, so a request-heavy server doesn't re-run your config function (e.g. re-$fetch a remote source) on every single request. Per-visitor A/B variant assignment is a cheap deterministic hash and is always recomputed fresh for every request from that request's own context — it is never cached or reused across visitors, on any deployment target (Node, serverless, edge, warm or cold instances).
Configuration reference
// nuxt.config.ts
export default defineNuxtConfig({
featureFlags: {
config: './feature-flags.config.ts', // path to a config file (optional)
flags: { /* inline flag definitions, merged with the config file */ },
cacheTTL: 5000, // ms the server caches flag definitions for (default: 1000) — see "How server-side caching works" above
},
})API
Client
useFeatureFlags()—{ flags, isEnabled(flag), getValue(flag), getVariant(flag) }useAsyncFeatureFlags()—{ flags, pending, error, refresh() }
Server
await getFeatureFlags(event)—{ flags, isEnabled(flag), getValue(flag), getVariant(flag) }
Directive
<template>
<div v-feature="'myFlag'">Only when enabled</div>
</template>v-feature removes the element from the DOM on mount if the flag is disabled. Because the removal happens client-side, the element is present in the initial server-rendered HTML and disappears right after hydration — prefer isEnabled() with v-if when that flash matters (e.g. above the fold).
Migration Notes (v1 -> v2)
- Server resolution is async:
await getFeatureFlags(event). - Async config functions are supported.
useAsyncFeatureFlagsis available for client refresh states.useFeatureFlagsreads already-resolved flags from Nuxt app context.
Testing
Current branch status:
npm run lintpasses.npm run testpasses (213tests).npx nuxt-module-build buildpasses, and a fullnuxi buildagainst the built package succeeds (non-blocking builder warnings may appear).
Documentation
License
MIT License (c) 2024
