Controlled feature rollouts without release anxiety
Feature flags separate deploying code from activating a feature for users. In Vue apps, this becomes a powerful tool for gradual rollouts, A/B tests and fast rollback in case of errors, provided the composable architecture, type safety and cleanup discipline are considered from the start.
Table of contents
- 1. Why feature flags in Vue apps are more than if statements
- 2. Designing a composable API for feature flags
- 3. Type safety: flags are not arbitrary strings
- 4. Remote configuration instead of a rebuild per flag change
- 5. Targeting: rollout percentages and user segments
- 6. A/B testing built on feature flags
- 7. Feature flags in SSR and Nuxt contexts
- 8. Cleaning up: removing flags before they become legacy
- 9. Feature flag approaches compared
- 10. Summary
- 11. FAQ
1. Why feature flags in Vue apps are more than if statements
A feature flag in a Vue app is at its core a conditional statement that decides whether a code path is active for a given user. The real value, however, only comes from decoupling deployment from activation: code for a new feature can be deployed to production without a single user seeing it until the flag is explicitly flipped. This significantly reduces the risk of large releases, because deployment and feature activation become two separate, independently controllable events.
Many Vue teams start with naive if conditions that check an environment variable directly. That works for individual, short lived flags but does not scale: without central management, scattered checks appear across dozens of components, naming conventions become unclear, and flags pile up that no one can safely remove anymore because their dependencies are no longer traceable. A well thought out feature flag system for Vue apps solves exactly these organizational problems, not just the technical condition check itself.
The second important benefit of feature flags in Vue apps is the ability to switch back instantly in case of an error. A feature that produces unexpected errors after rollout can be disabled through the flag system within seconds, without performing a rollback deployment. This response time is often the decisive difference between a small incident and a larger revenue loss in critical applications such as checkout.
2. Designing a composable API for feature flags
The obvious Vue 3 solution for feature flags is a composable that centrally holds the state of all flags and makes it queryable through a unified interface. Instead of checking in every component individually where the flag data comes from, useFeatureFlag(flagName) encapsulates the entire logic: loading configuration, caching, fallback behavior on network errors, and reactive updates if a flag changes at runtime.
It is important that the composable stays reactive, that is, it returns a ref or computed, not a static boolean. This allows a feature flag to be toggled at runtime, for example via an admin panel trigger, and the affected Vue component updates automatically without needing a page reload. For server side rendering with Nuxt, the composable must additionally be synchronized between server and client context to avoid hydration mismatches.
// composables/useFeatureFlag.js — Central, reactive feature flag access
import { computed } from 'vue';
import { useFeatureFlagStore } from '@/stores/featureFlags';
export function useFeatureFlag(flagName) {
const store = useFeatureFlagStore();
const isEnabled = computed(() => {
// Falls back to false if the flag is unknown — never throws in production
return store.flags[flagName]?.enabled ?? false;
});
const variant = computed(() => {
return store.flags[flagName]?.variant ?? 'control';
});
return { isEnabled, variant };
}
// Usage inside a Vue component
import { useFeatureFlag } from '@/composables/useFeatureFlag';
const { isEnabled: showNewCheckout } = useFeatureFlag('new-checkout-flow');
3. Type safety: flags are not arbitrary strings
A common problem with feature flags in Vue apps across many teams is a typo in the flag name: useFeatureFlag('new-checkot-flow') instead of 'new-checkout-flow' compiles without complaint, but the feature stays permanently disabled without any visible error. TypeScript solves this by defining all known flag names as a union type or an enum, so the compiler immediately flags any incorrect flag name.
For larger Vue codebases, it is additionally recommended to maintain flag definitions centrally in a single file, ideally generated from the feature flag provider's own configuration, so frontend types and backend configuration never drift apart. A CI step that checks whether all flag names used in code actually exist in the provider additionally catches orphaned or misspelled flags before they go unnoticed in production.
// types/featureFlags.ts — Central, typed registry of all known flags
export type FeatureFlagName =
| 'new-checkout-flow'
| 'dark-mode-toggle'
| 'ai-product-recommendations'
| 'express-shipping-banner';
export interface FeatureFlagState {
enabled: boolean;
variant?: string;
}
// Typed composable signature — a typo in the flag name fails at compile time
export function useFeatureFlag(flagName: FeatureFlagName): {
isEnabled: import('vue').ComputedRef<boolean>;
variant: import('vue').ComputedRef<string>;
};
4. Remote configuration instead of a rebuild per flag change
Feature flags controlled only through build time environment variables require a new deployment cycle for every change, which undoes the biggest advantage of feature flags, the decoupling of deployment and activation. The more robust architecture loads flag configuration from a remote service, either a dedicated feature flag provider such as LaunchDarkly and Unleash, or a simple, self hosted JSON endpoint queried at Vue application startup.
It matters for user experience that this remote query does not block the initial render. A proven pattern: flags load asynchronously while the application renders with sensible defaults, and updates reactively once the actual configuration arrives. For critical flags that affect the initial layout, such as the visibility of a new header, brief preloading via a synchronous server call in Nuxt can make sense to avoid layout shifts.
// stores/featureFlags.js — Loading remote flag configuration with sane fallbacks
import { defineStore } from 'pinia';
export const useFeatureFlagStore = defineStore('featureFlags', {
state: () => ({
flags: {},
isLoaded: false,
}),
actions: {
async loadFlags(userId) {
try {
const response = await fetch(`/api/feature-flags?userId=${userId}`, {
signal: AbortSignal.timeout(2000), // never block the app for long
});
this.flags = await response.json();
} catch {
// Network failure — keep default flags (all disabled) rather than crashing
console.warn('[feature-flags] Falling back to defaults');
} finally {
this.isLoaded = true;
}
},
},
});
5. Targeting: rollout percentages and user segments
A simple on/off flag rarely suffices for a controlled rollout. In practice, feature flag targeting requires at least two dimensions: percentage rollouts, where a feature is initially active for 5 percent of users and gradually increased, and segment targeting, where certain user groups, such as internal staff or beta testers, see the feature independent of the percentage rollout.
Assigning a user to a rollout percentage must be deterministic and stable, otherwise a user would flip between enabled and disabled state on every page visit. A proven feature flag pattern is a hash of the user ID modulo 100, compared against the rollout percentage. This guarantees that the same user always sees the same state as long as the rollout percentage stays constant, while the user group only ever expands, never changes, when the percentage is increased.
// utils/rolloutBucket.js — Deterministic, stable user assignment for percentage rollouts
export function isInRollout(userId, flagName, rolloutPercentage) {
// Combine userId and flagName so the same user gets independent buckets per flag
const key = `${userId}:${flagName}`;
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = (hash << 5) - hash + key.charCodeAt(i);
hash |= 0; // force 32-bit integer
}
const bucket = Math.abs(hash) % 100;
return bucket < rolloutPercentage; // stable: same userId + flagName -> same bucket
}
// Segment targeting takes priority over percentage rollout
export function resolveFlag(user, flagConfig) {
if (flagConfig.segments?.includes(user.segment)) return true;
return isInRollout(user.id, flagConfig.name, flagConfig.rolloutPercentage ?? 0);
}
6. A/B testing built on feature flags
Feature flags and A/B tests share the same technical foundation but differ in goal: a feature flag controls whether a feature is visible, an A/B test measures which of several variants performs better. In Vue apps, this can be modeled with the same composable by having a flag return not just enabled, but additionally variant, for example 'control' or 'treatment'.
Decisive for reliable A/B test results is that variant assignment is logged only after the assignment happens, not already at flag query time. If every flag query automatically triggered an analytics event, components that query a flag multiple times, for example on every re-render, would distort the metrics. The correct order: determine the variant once at the first visible render of the affected UI element and report it exactly once to the analytics system.
// composables/useAbTestExposure.js — Log the variant exactly once, on first visible render
import { onMounted } from 'vue';
import { useFeatureFlag } from '@/composables/useFeatureFlag';
export function useAbTestExposure(flagName) {
const { isEnabled, variant } = useFeatureFlag(flagName);
onMounted(() => {
// Fires once per component mount, not on every reactive re-evaluation
analytics.track('experiment_exposure', {
experiment: flagName,
variant: variant.value,
});
});
return { isEnabled, variant };
}
7. Feature flags in SSR and Nuxt contexts
In Nuxt applications with server side rendering, feature flags must resolve consistently between server and client, otherwise hydration mismatches occur: the server renders one variant, the client hydrates with another, because the flag query returns a different result client side. The solution is to resolve flags server side exactly once, for example in a Nuxt plugin or middleware, and pass the result to the client via Nuxt's state transfer mechanism.
For user specific targeting, for example based on a session cookie, this resolution must happen again on every server request but must never be cached and reused for other users. An edge level cache for feature flag responses is therefore only permissible if the cache key correctly incorporates the relevant targeting dimensions, such as user segment.
8. Cleaning up: removing flags before they become legacy
The biggest long term risk factor for feature flags in Vue apps is not the introduction but forgetting the removal. A flag that has been active for 100 percent of users for months should be removed, both from the provider and from the Vue code, because every remaining flag represents an additional, untested code branch. Without discipline, dozens of dead flags accumulate over time, making the codebase unnecessarily complex and hindering refactoring.
A pragmatic process defines an expiration date for every feature flag from the start, tracked in the ticket system. After full rollout to 100 percent and an observation period of one to two weeks, the flag is removed: the if condition in the Vue code resolved, the no longer needed code path deleted, and the flag entry archived in the provider. Some teams even automate this reminder through a linting tool that flags entries older than a defined time window.
9. Feature flag approaches compared
Various approaches are available for technically implementing feature flags in Vue apps, from simple environment variables to dedicated SaaS platforms with full targeting and analytics.
| Approach | Change without rebuild | Targeting | Effort |
|---|---|---|---|
| Build time environment variable | No | None | Minimal |
| Custom JSON endpoint | Yes | Manually implementable | Medium |
| Unleash (open source) | Yes | Extensive | Medium, self hosting |
| LaunchDarkly / Split (SaaS) | Yes | Very extensive | Cost, vendor lock-in |
For smaller Vue projects with few active flags, a self hosted JSON endpoint is often enough. Once rollout percentages, user segments and A/B test evaluation are needed simultaneously, investing in Unleash or a SaaS provider usually pays off quickly through less in-house build effort.
Mironsoft
Feature flag architecture, controlled rollouts and Vue release strategy
Roll out features risk free, without big bang releases?
We build a type safe feature flag architecture for your Vue apps, including remote configuration, rollout targeting and a cleanup process that prevents flags from turning into legacy code.
Architecture
Composable based, type safe feature flag integration in Vue and Nuxt
Rollout strategy
Percentage rollouts, segment targeting and A/B test evaluation
Cleanup process
Expiration dates and linting so flags do not stay in code permanently
10. Summary
Feature flags in Vue apps decouple deployment and feature activation, enabling gradual rollouts, instant rollback in case of errors, and data driven A/B tests. A central composable API with reactive return values keeps usage consistent across the codebase, while TypeScript types for flag names catch typos already at compile time. Remote configuration instead of build time variables is a prerequisite for flags actually being changeable without a rebuild.
Deterministic rollout targeting through hash based user assignment ensures a stable user experience during a gradual rollout. In SSR contexts with Nuxt, flag resolution must be kept consistent between server and client to avoid hydration mismatches. The often underestimated but decisive final step is the disciplined removal of fully rolled out flags, without which every feature flag introduction eventually creates new technical debt.
Feature Flags in Vue Apps — the essentials at a glance
Composable API
Central, reactive useFeatureFlag instead of scattered if conditions in components.
Type safety
Flag names as a union type, so typos surface at compile time, not first in production.
Targeting
Hash based, deterministic rollout percentages plus segment targeting for beta users.
Cleanup
Fixed expiration date per flag, otherwise dead code branches accumulate permanently.