nuxt-feature-flags
| Install | |
|---|---|
npm install nuxt-feature-flags |
|
| Latest Version: | 3.0.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 18, 2026 |
| Links: | GitHub · npm |
Nuxt Feature Flags
Type-safe feature flags for Nuxt 3 and Nuxt 4, evaluated on the server per request, with built-in A/B/n testing.
Highlights
- Flags declared inline in
nuxt.config.tsor in a config file — a plain object, or a (sync or async) function of the request context. - Typed flag names:
isEnabled('newDashbaord')is a type error. Names are inferred from your config file and inline flags. - Sticky A/B/n variants (hashed by user id, session cookie or IP — no external service), checked with
isEnabled('experiment:variant'). - Resolved once per request on the server and hydrated on the client: no extra request, no flash.
- Only each visitor's resolved flags reach the browser — never the definitions (weights, disabled flags...).
useFeatureFlags(),useAsyncFeatureFlags(), av-featuredirective and agetFeatureFlags(event)server util, all auto-imported.
Installation
npx nuxi module add nuxt-feature-flags
Quick Start
1. Declare your flags
Inline in nuxt.config.ts:
export default defineNuxtConfig({
modules: ['nuxt-feature-flags'],
featureFlags: {
flags: {
newDashboard: true,
checkoutExperiment: {
enabled: true,
variants: [
{ name: 'control', weight: 50, value: 'control' },
{ name: 'treatment', weight: 50, value: 'treatment' },
],
},
},
},
})
...or in a config file, which can also compute flags from the request:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-feature-flags'],
featureFlags: {
config: './feature-flags.config.ts',
},
})
// feature-flags.config.ts
import { defineFeatureFlags } from '#feature-flags/handler'
export default defineFeatureFlags(async context => ({
// `context` is the request's `event.context`: whatever your server middleware
// attached to it (auth user, tenant, Cloudflare bindings...).
isAdmin: context.user?.role === 'admin',
newDashboard: true,
remoteFlag: await $fetch<boolean>('https://flags.example.com/remote-flag'),
}))
A function config is evaluated on the server for every request (see cacheTTL to share results), inside Nitro, so it can use anything available there (useStorage, useRuntimeConfig, bindings...). It never runs at build time. If it throws, the page still renders: flags fall back to the cached result (when cacheTTL is set) or to the inline flags, and the error is logged.
If a flag is declared both inline and in the config file, the config file wins.
2. Use them in components
<script setup lang="ts">
const { isEnabled, getValue, getVariant } = useFeatureFlags()
</script>
<template>
<NewDashboard v-if="isEnabled('newDashboard')" />
<CheckoutV2 v-if="isEnabled('checkoutExperiment:treatment')" />
<CheckoutV1 v-else />
<p>Variant: {{ getVariant('checkoutExperiment') }} ({{ getValue('checkoutExperiment') }})</p>
</template>
3. Use them on the server
// server/api/dashboard.ts — `getFeatureFlags` is auto-imported in server/
export default defineEventHandler(async (event) => {
const { isEnabled, getVariant } = await getFeatureFlags(event)
if (!isEnabled('newDashboard')) {
throw createError({ statusCode: 404 })
}
return { variant: getVariant('checkoutExperiment') }
})
Flags are evaluated once per request: the page render, your API routes and middleware share the same result. Set event.context.user in a server middleware before reading flags so the config function and variant assignment can use it.
Flag definitions
| Definition | Resolves to |
|---|---|
true / 'blue' / 42 |
enabled when truthy, with that value |
{ enabled: false, value } |
disabled, never assigned a variant |
{ enabled: true, value } |
enabled, with value |
{ value } (no enabled) |
enabled when value is truthy |
{ enabled: true, variants } |
enabled, value of the visitor's variant (or the flag's value if the variant has none). A variant with value: false turns the flag off for its visitors. Visitors outside every variant (when weights add up to less than 100) get the flag's own value and no variant |
Variants (A/B/n testing)
Each visitor is bucketed by hashing flagName + identifier, where the identifier is the first available of event.context.user.id (or event.context.userId), a session cookie (session_id, session-id, nuxt-session), or the client IP. The same visitor always gets the same variant of a flag, and buckets of different flags are independent.
Weights are percentages of traffic. When they add up to less than 100, the remaining visitors get no variant and see the flag's own value, which makes gradual rollouts a one-liner:
newCheckout: {
enabled: true,
value: false, // the other 80%: flag off, no variant
variants: [{ name: 'rollout', weight: 20, value: true }], // 20% of visitors
},
Raise the weight to roll out further: visitors already in the rollout stay in it. Weights adding up to more than 100 are scaled down to fit (and reported as a validation error).
const { isEnabled, getVariant } = useFeatureFlags()
isEnabled('checkoutExperiment') // is the flag on for this visitor?
isEnabled('checkoutExperiment:treatment') // is this visitor in the "treatment" variant?
getVariant('checkoutExperiment') // 'control' | 'treatment' | undefined
API
useFeatureFlags()
{ flags, isEnabled(flag), getValue(flag), getVariant(flag) } over the flags resolved on the server. Reactive: it updates when the flags are refreshed.
useAsyncFeatureFlags(options?)
Same helpers plus { flags, pending, error, refresh() }. refresh() re-evaluates the flags on the server (e.g. after login) and updates every component using flags. By default it refreshes when the component mounts; pass { immediate: false } to only refresh on demand.
v-feature
<div v-feature="'newDashboard'">Only when enabled</div>
<div v-feature="'checkoutExperiment:treatment'">Only in the treatment variant</div>
Works like v-show: the element is rendered with display: none when the flag is off — already in the server HTML, so there's no flash — and toggles when flags are refreshed. Use v-if="isEnabled(...)" to keep the markup out of the page entirely.
getFeatureFlags(event) (server)
Promise<{ flags, isEnabled, getValue, getVariant }>. Auto-imported in server/; elsewhere, import { getFeatureFlags } from '#feature-flags/server/utils'.
GET /api/_feature-flags/feature-flags
The current visitor's resolved flags as JSON (sent with cache-control: private, no-store). Used by client-rendered pages and refresh().
Configuration
export default defineNuxtConfig({
featureFlags: {
config: './feature-flags.config.ts', // flags config file, relative to the project root (optional)
flags: {}, // inline flags (optional)
cacheTTL: 0, // ms a function config's result may be shared across requests (default 0)
},
})
A config path that doesn't exist fails the build with a clear error instead of silently running without flags.
Inline flags live in the server-only runtime config, so they can be overridden per environment without rebuilding, e.g. NUXT_FEATURE_FLAGS_FLAGS_NEW_DASHBOARD=false.
Caching
By default a function config runs on every request, so it can safely depend on the visitor. If it doesn't (e.g. it only fetches flags from a remote service), set cacheTTL to reuse its result for that many milliseconds — the cached result is shared by every visitor, so don't combine cacheTTL with per-visitor logic. Concurrent requests during a cache miss share a single evaluation. Variant assignment is always computed per visitor, cache or not.
Type safety
Flag names are generated into .nuxt/types/nuxt-feature-flags.d.ts from your inline flags and from the return type of your config file, so isEnabled, getValue, getVariant and getFeatureFlags only accept declared flags (plus flag:variant forms). If names can't be inferred (e.g. the config is typed as a generic record), any string is accepted.
Build-time validation
nuxt-feature-flags/build checks the config (names, variant weights...) and that every flag used in your code (isEnabled('x'), v-feature="'x'"...) is declared. Handy in CI:
// scripts/validate-flags.ts
import { validateFeatureFlags } from 'nuxt-feature-flags/build'
await validateFeatureFlags({
configPath: 'feature-flags.config.ts',
srcPatterns: ['app/**/*.{vue,ts}', 'server/**/*.ts'],
failOnErrors: true,
})
In development, invalid definitions are also reported in the server console.
Rendering modes
| Mode | Support |
|---|---|
| SSR (default) | ✅ Flags resolved per request with the real request context, then hydrated. |
SPA (ssr: false or routeRules: { ssr: false }, with a server) |
✅ The client fetches the flags from the API route before rendering. |
Hybrid (isr / swr / prerender route rules) |
✅ Server-rendered routes behave like SSR. Remember that cached/prerendered HTML is shared, so its flags and variants are too. |
Static generation (nuxi generate) |
⚠️ Flags are computed once at build time and baked into the HTML: fine for static flags, but every visitor of a page gets the same variant. |
| Fully static hosting of an SPA (no server at all) | ❌ The flags API route needs a server. |
Migrating from 2.0
- Variant weights adding up to less than 100 are no longer scaled up: the remainder of visitors gets no variant. Before,
[{ name: 'new', weight: 20 }]sent 100% of visitors tonew; now it sends 20%. Configs whose weights add up to exactly 100 behave as before, and visitors keep their variants. isEnabled('flag:variant')now works everywhere (it always returnedfalsebefore).- A function config is no longer cached for 1s by default — that cache shared one visitor's flags with others. Set
cacheTTLexplicitly if your config doesn't depend on the visitor. - Inline flags moved from
runtimeConfig.public.featureFlagsto the server-onlyruntimeConfig.featureFlags(env overrides:NUXT_FEATURE_FLAGS_FLAGS_*instead ofNUXT_PUBLIC_FEATURE_FLAGS_FLAGS_*). v-featurehides elements (likev-show) instead of removing them after hydration.- A missing
configfile now fails the build. - The undocumented
isFeatureEnabledserver helper was removed; useawait getFeatureFlags(event). - Server-rendered pages no longer fetch the flags again from the browser after hydration.
Documentation
License
MIT License (c) 2024
Related Packages
Nuxt module for A vs B feature flags: one line in nuxt.config, auto-imported com...
Frontend helper for Laravel Pennant feature flags (with support for React and Vu...
Composable page capabilities for Nuxt with typed config inheritance and Nuxt Con...
Version History
| Version | Released | License |
|---|---|---|
| 3.0.0 | MIT | |
| 2.0.5 | MIT | |
| 2.0.4 | MIT | |
| 2.0.3 | MIT | |
| 2.0.2 | MIT | |
| 2.0.1 | MIT | |
| 1.1.7 | MIT | |
| 1.1.6 | MIT | |
| 1.1.5 | MIT | |
| 1.1.4 | MIT | |
| 1.1.3 | MIT | |
| 1.1.2 | MIT | |
| 1.1.1 | MIT | |
| 1.1.0 | MIT | |
| 1.0.2 | MIT | |
| 1.0.1 | MIT | |
| 1.0.0 | MIT |