How to Use Feature Flags in Frontend Apps
Feature flags are a deployment tool first and a UI tool second. The idea is simple: merge code to your main branch, ship it to production hidden behind a condition, then turn it on when - and for whom - you're ready.
This guide covers the core patterns in plain JavaScript using the @supashiphq/javascript-sdk. Every example here works in any frontend framework - or no framework at all. See the full JavaScript SDK reference for the complete API. If you're using React, Vue, or Next.js, we have dedicated guides at the end.
Installation
pnpm add @supashiphq/javascript-sdk
Or load it straight from a CDN with no build step:
<script type="module">
import { SupaClient } from 'https://cdn.skypack.dev/@supashiphq/javascript-sdk'
</script>
Step 1: Create a client
Create the client once when your app initialises. Every method on it is async, so you can call it from anywhere - inside a component lifecycle, a router hook, or plain DOMContentLoaded.
// lib/features.ts
import { SupaClient, FeaturesWithFallbacks } from '@supashiphq/javascript-sdk'
export const FEATURE_FLAGS = {
'new-navigation': false,
'dark-mode': false,
'premium-features': false,
'announcement-banner': false,
} satisfies FeaturesWithFallbacks
export const featureClient = new SupaClient({
apiKey: import.meta.env.VITE_SUPASHIP_API_KEY, // Client API Key (public, safe for the browser)
environment: import.meta.env.MODE, // 'development' | 'production' etc.
features: FEATURE_FLAGS,
context: {},
})
Two things worth noting up front:
Use a Client API Key, not a Server API Key. The client key is designed to be public and embedded in browser code. Server keys should only ever live on the backend.
satisfies FeaturesWithFallbacks preserves the exact types of your fallback values, so getFeature returns the right TypeScript type for each flag. Fallbacks are also what users see if the API is unreachable - choose values that preserve your current behaviour.
Step 2: Toggle a feature
import { featureClient } from './lib/features'
const showNewNav = await featureClient.getFeature('new-navigation')
if (showNewNav) {
document.querySelector('#nav-v2')?.classList.remove('hidden')
} else {
document.querySelector('#nav-v1')?.classList.remove('hidden')
}
That's the whole model. Everything else is a variation of this pattern.
Step 3: Fetch multiple flags at once
Each getFeature call is a network request. When you need several flags on the same screen, use getFeatures to get them all in a single round-trip:
// ❌ Three network requests
const newNav = await featureClient.getFeature('new-navigation')
const darkMode = await featureClient.getFeature('dark-mode')
const premium = await featureClient.getFeature('premium-features')
// ✅ One network request
const features = await featureClient.getFeatures([
'new-navigation',
'dark-mode',
'premium-features',
])
if (features['new-navigation']) showNewNavigation()
if (features['dark-mode']) applyDarkTheme()
if (features['premium-features']) showPremiumContent()
A natural place to do this is during app initialisation - fetch everything you need before the first render, then read from the results synchronously throughout your app.
// app.ts - vanilla JS initialisation
async function bootstrap() {
const features = await featureClient.getFeatures([
'new-navigation',
'dark-mode',
'announcement-banner',
'premium-features',
])
if (features['dark-mode']) document.body.classList.add('dark')
if (features['announcement-banner']) renderBanner()
if (features['new-navigation']) renderNewNav()
else renderLegacyNav()
renderApp() // render the rest of the app once flags are known
}
document.addEventListener('DOMContentLoaded', bootstrap)
Step 4: Add user context for targeting
A flag evaluated with no context is a global on/off switch. Passing context lets Supaship apply targeting rules - "only enable this for users on the Pro plan", "roll out to 10% of users", "show this only in the US".
Set the user context once you know who the current user is:
// After login resolves
featureClient.updateContext({
userId: user.id,
email: user.email,
plan: user.plan, // 'free' | 'pro' | 'enterprise'
appVersion: APP_VERSION,
})
Then fetch flags as normal - the SDK sends the context with every request:
const premiumEnabled = await featureClient.getFeature('premium-features')
// Evaluated against { userId, email, plan, appVersion }
You can also pass a one-off context override for a single call without changing the shared state:
const betaAccess = await featureClient.getFeature('beta-access', {
context: { plan: 'enterprise', region: 'us-east-1' },
})
Object-valued flags for configuration
Feature flags don't have to be booleans. A flag can carry an entire configuration object that you update from the dashboard without changing code:
const FEATURE_FLAGS = {
'ui-settings': {
theme: 'light' as 'light' | 'dark',
accentColor: '#007bff',
sidebarCollapsed: false,
},
'announcement-banner': false,
} satisfies FeaturesWithFallbacks
const uiSettings = await featureClient.getFeature('ui-settings')
// Type: { theme: 'light' | 'dark'; accentColor: string; sidebarCollapsed: boolean }
document.documentElement.dataset.theme = uiSettings.theme
document.documentElement.style.setProperty('--accent', uiSettings.accentColor)
This is useful for things like theming, copy experiments, and rate limits - anything where you want the flexibility to change the value without a deploy.
Flags with null for an unset state
Sometimes the right default is "nothing" - the feature shouldn't appear at all until you explicitly turn it on. Use null as the fallback:
const FEATURE_FLAGS = {
'promo-banner-text': null,
} satisfies FeaturesWithFallbacks
const promoText = await featureClient.getFeature('promo-banner-text')
// Type: null (or whatever value you configure in the dashboard)
if (promoText !== null) {
renderBanner(promoText)
}
Updating context when user state changes
If your app has auth, the user context changes during the session (anonymous → logged in → upgraded plan). Call updateContext whenever the relevant state changes:
// Merge new values into the existing context (default)
featureClient.updateContext({ userId: user.id, plan: user.plan })
// Replace the context entirely (pass false as second argument)
featureClient.updateContext({ userId: user.id }, false)
// Read the current context
const ctx = featureClient.getContext()
After updating, call getFeatures again to get flag values re-evaluated against the new context. This is especially important for plan-gated features - a user who upgrades mid-session should get their new flags without a page reload.
async function onPlanUpgrade(newPlan: string) {
featureClient.updateContext({ plan: newPlan })
// Re-fetch the flags that depend on plan
const features = await featureClient.getFeatures(['premium-features'])
if (features['premium-features']) showPremiumContent()
}
A minimal real-world example (vanilla JS)
Here's everything above put together into a small but complete vanilla JS app:
// app.ts
import { SupaClient, FeaturesWithFallbacks } from '@supashiphq/javascript-sdk'
const FEATURE_FLAGS = {
'new-navigation': false,
'dark-mode': false,
'announcement-banner': null,
'ui-settings': { accentColor: '#007bff' },
} satisfies FeaturesWithFallbacks
const featureClient = new SupaClient({
apiKey: import.meta.env.VITE_SUPASHIP_API_KEY,
environment: import.meta.env.MODE,
features: FEATURE_FLAGS,
context: {},
})
async function bootstrap() {
// Resolve user from session/cookie
const user = await getCurrentUser()
// Provide context before fetching flags
if (user) {
featureClient.updateContext({
userId: user.id,
email: user.email,
plan: user.plan,
})
}
// Fetch all flags in one request
const features = await featureClient.getFeatures([
'new-navigation',
'dark-mode',
'announcement-banner',
'ui-settings',
])
// Apply results
if (features['dark-mode']) {
document.body.classList.add('dark')
}
if (features['announcement-banner'] !== null) {
renderBanner(features['announcement-banner'])
}
const accent = features['ui-settings'].accentColor
document.documentElement.style.setProperty('--accent', accent)
if (features['new-navigation']) renderNewNav()
else renderLegacyNav()
}
document.addEventListener('DOMContentLoaded', bootstrap)
Using the development toolbar
The SDK ships with a visual toolbar that lets you override flag values locally without touching the dashboard. It appears automatically on localhost during development:
// Toolbar is auto-enabled on localhost - no config needed.
// To always show it:
const featureClient = new SupaClient({
apiKey: import.meta.env.VITE_SUPASHIP_API_KEY,
environment: import.meta.env.MODE,
features: FEATURE_FLAGS,
context: {},
toolbar: {
enabled: true,
position: { placement: 'bottom-right' },
},
})
// To disable it in production:
toolbar: false
The toolbar shows all configured flags, their current values, and lets you override them per-session. Overrides are stored in localStorage and cleared when you reset from the toolbar. It's the fastest way to test both branches of a flag without writing a test or changing code.
What to do when you're done with a flag
A flag that's been globally enabled for weeks is just an if statement with extra steps. Once a rollout is complete, clean up:
- Remove the flag condition from your code
- Delete the flag in the Supaship dashboard
- Remove it from your
FEATURE_FLAGSdefinition
TypeScript helps here - if you delete a flag from FEATURE_FLAGS, every getFeature('that-flag') call becomes a compile error, guiding you to all the places that need cleaning up.
Framework-specific guides
The patterns above work in any framework. We have dedicated guides for the most common stacks:
- Feature Flags in React —
SupaProvider,useFeature,useFeatures, user targeting, and testing with mocked flags. Full React SDK reference. - Feature Flags in Next.js — App Router, Server Components, Edge Middleware, and avoiding hydration mismatches. Full Next.js SDK reference.
- Feature Flags in Vue — Vue 3 Composition API, Vue Router, and Nuxt 3. Full Vue SDK reference.
- Feature Flags in Node.js — Express, Fastify, serverless, and request-scoped flag evaluation. Full Node.js SDK reference.
Ready to try it? Create a free Supaship account and have your first flag live in minutes. Free forever up to 1M events/month. Pro plan is $30/month for your entire workspace - unlimited projects, unlimited team members, and unlimited environments.
Comparing platforms? Supaship vs LaunchDarkly · Supaship vs Statsig · Supaship vs ConfigCat · Best Feature Flag Platforms 2026
Feedback
Got thoughts on this?
We're constantly learning how developers actually use these tools. Ideas, use cases, integration requests — every bit of feedback makes the platform better for everyone.
Thanks for being part of the journey — Supaship