Nuxt
Complete guide for integrating Supaship feature flags with Nuxt 3, including server-side rendering, composables, and middleware support.
Requirements
- Nuxt 3.0+
- Vue 3.0+ (Composition API support)
- Use
@supashiphq/vue-sdkpackage
Installation
pnpm add @supashiphq/vue-sdkQuick Start
Setup Plugin
// plugins/supaship.client.ts
import { defineNuxtPlugin } from '#app'
import { createSupaship, FeaturesWithFallbacks, InferFeatures } from '@supashiphq/vue-sdk'
const FEATURE_FLAGS = {
'new-homepage': false,
'theme-config': { mode: 'light' as const, showLogo: true },
'beta-features': [] as string[],
} satisfies FeaturesWithFallbacks
// REQUIRED: for type safety
declare module '@supashiphq/vue-sdk' {
interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
}
export default defineNuxtPlugin(nuxtApp => {
const config = useRuntimeConfig()
const supaship = createSupaship({
config: {
apiKey: config.public.supashipApiKey as string,
environment: process.env.NODE_ENV || 'production',
features: FEATURE_FLAGS,
context: {
version: config.public.appVersion as string,
},
},
})
nuxtApp.vueApp.use(supaship)
})Configuration
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
supashipApiKey: process.env.NUXT_PUBLIC_SUPASHIP_API_KEY || '',
appVersion: process.env.NUXT_PUBLIC_APP_VERSION || '1.0.0',
},
},
})Environment Variables
Create a .env file:
NUXT_PUBLIC_SUPASHIP_API_KEY=your-api-key-here
NUXT_PUBLIC_APP_VERSION=1.0.0Using in Components
<!-- pages/index.vue -->
<script setup lang="ts">
import { useFeature } from '@supashiphq/vue-sdk'
const { feature: newHomepage, isLoading } = useFeature('new-homepage')
</script>
<template>
<div v-if="isLoading">Loading...</div>
<NewHomepage v-else-if="newHomepage" />
<OldHomepage v-else />
</template>Type-Safe Feature Flags
Centralize your feature definitions:
// composables/features.ts
import { FeaturesWithFallbacks, InferFeatures } from '@supashiphq/vue-sdk'
export const FEATURE_FLAGS = {
'new-dashboard': false,
'theme-config': {
mode: 'light' as 'light' | 'dark',
primaryColor: '#007bff',
showLogo: true,
},
'beta-features': [] as string[],
'disabled-feature': null,
} satisfies FeaturesWithFallbacks
// Type augmentation for global type safety
declare module '@supashiphq/vue-sdk' {
interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
}Then use in your plugin:
// plugins/supaship.client.ts
import { FEATURE_FLAGS } from '~/composables/features'
export default defineNuxtPlugin(nuxtApp => {
// ... plugin setup with FEATURE_FLAGS
})Server-Side Rendering
Server Plugins
For server-side feature flag checks, use the JavaScript SDK:
// plugins/supaship.server.ts
import { createClient } from '@supashiphq/js-sdk'
export default defineNuxtPlugin(async () => {
const config = useRuntimeConfig()
const client = createClient({
apiKey: config.public.supashipApiKey as string,
environment: process.env.NODE_ENV || 'production',
})
return {
provide: {
supaship: client,
},
}
})Server Components / Pages
Use server composable or direct client access:
<!-- pages/dashboard.vue -->
<script setup lang="ts">
const { $supaship } = useNuxtApp()
const user = await useUser()
// Fetch features on the server
const features = await $supaship.getFeatures(
['new-dashboard', 'beta-mode'],
{
context: {
userId: user.id,
email: user.email,
},
},
)
</script>
<template>
<div>
<NewDashboard v-if="features['new-dashboard']" />
<OldDashboard v-else />
<BetaBadge v-if="features['beta-mode']" />
</div>
</template>Server API Routes
// server/api/features.ts
import { createClient } from '@supashiphq/js-sdk'
export default defineEventHandler(async event => {
const config = useRuntimeConfig()
const query = getQuery(event)
const client = createClient({
apiKey: config.public.supashipApiKey as string,
environment: process.env.NODE_ENV || 'production',
})
const features = await client.getFeatures(['feature-a', 'feature-b'], {
context: {
userId: query.userId as string,
},
})
return features
})Composable Patterns
Reactive User Context
Update context when user state changes:
// composables/useFeatureContext.ts
import { watch } from 'vue'
import { useFeatureContext } from '@supashiphq/vue-sdk'
import { useAuth } from './useAuth'
export function useReactiveFeatureContext() {
const { updateContext } = useFeatureContext()
const { user } = useAuth()
watch(
user,
newUser => {
if (newUser) {
updateContext({
userId: newUser.id,
email: newUser.email,
plan: newUser.plan,
segment: newUser.segment,
})
}
},
{ immediate: true },
)
}Use in your app:
<!-- app.vue -->
<script setup lang="ts">
useReactiveFeatureContext()
</script>
<template>
<NuxtPage />
</template>Multiple Features
<script setup lang="ts">
import { computed } from 'vue'
import { useFeatures } from '@supashiphq/vue-sdk'
import { useUser } from './composables/useUser'
const { user } = useUser()
const { features, isLoading } = useFeatures(
['new-dashboard', 'beta-mode', 'show-sidebar'],
{
context: computed(() => ({
userId: user.value?.id,
plan: user.value?.plan,
})),
},
)
</script>
<template>
<LoadingSpinner v-if="isLoading" />
<div v-else :class="features['new-dashboard'] ? 'new-layout' : 'old-layout'">
<Sidebar v-if="features['show-sidebar']" />
<BetaBadge v-if="features['beta-mode']" />
<MainContent />
</div>
</template>Middleware
Route Guards with Feature Flags
// middleware/feature-guard.ts
import { createClient } from '@supashiphq/js-sdk'
export default defineNuxtRouteMiddleware(async (to, from) => {
const config = useRuntimeConfig()
const featureFlag = to.meta.requiresFeature as string | undefined
if (featureFlag) {
const client = createClient({
apiKey: config.public.supashipApiKey as string,
environment: process.env.NODE_ENV || 'production',
})
// Get user from cookie or session
const user = useCookie('user')
const feature = await client.getFeature(featureFlag, {
context: {
userId: user.value?.id,
},
})
if (!feature) {
return navigateTo('/404')
}
}
})
// pages/beta.vue
<script setup lang="ts">
definePageMeta({
middleware: 'feature-guard',
requiresFeature: 'beta-access',
})
</script>Conditional Redirects
// middleware/redirect.ts
export default defineNuxtRouteMiddleware(async to => {
const { $supaship } = useNuxtApp()
const user = useCookie('user')
const feature = await $supaship.getFeature('new-dashboard', {
context: { userId: user.value?.id },
})
if (to.path === '/dashboard' && feature) {
return navigateTo('/dashboard-v2')
}
})Advanced Patterns
Server-Side Feature Prefetching
Prefetch features in server middleware:
// server/middleware/features.ts
import { createClient } from '@supashiphq/js-sdk'
export default defineEventHandler(async event => {
const config = useRuntimeConfig()
const user = getCookie(event, 'user')
if (user) {
const client = createClient({
apiKey: config.public.supashipApiKey as string,
environment: process.env.NODE_ENV || 'production',
})
const features = await client.getFeatures(['feature-a', 'feature-b'], {
context: { userId: JSON.parse(user).id },
})
// Attach to event context for use in pages
event.context.features = features
}
})Plugin with User Context
// plugins/supaship.client.ts
import { defineNuxtPlugin } from '#app'
import { createSupaship, FeaturesWithFallbacks } from '@supashiphq/vue-sdk'
import { useAuth } from './composables/useAuth'
const FEATURE_FLAGS = {
'new-dashboard': false,
} satisfies FeaturesWithFallbacks
export default defineNuxtPlugin(async nuxtApp => {
const config = useRuntimeConfig()
const { user } = useAuth()
const supaship = createSupaship({
config: {
apiKey: config.public.supashipApiKey as string,
environment: process.env.NODE_ENV || 'production',
features: FEATURE_FLAGS,
context: {
userId: user.value?.id,
email: user.value?.email,
plan: user.value?.plan,
},
},
})
nuxtApp.vueApp.use(supaship)
})Feature Flag Composable
Create a custom composable for common patterns:
// composables/useFeatureGuard.ts
import { computed } from 'vue'
import { useFeature } from '@supashiphq/vue-sdk'
import { useRouter } from 'vue-router'
export function useFeatureGuard(
featureName: string,
redirectTo: string = '/404',
) {
const router = useRouter()
const { feature, isLoading } = useFeature(featureName)
const hasAccess = computed(() => {
if (isLoading.value) return null
return feature.value === true
})
watch(
hasAccess,
access => {
if (access === false) {
router.push(redirectTo)
}
},
{ immediate: true },
)
return { hasAccess, isLoading }
}Usage:
<script setup lang="ts">
import { useFeatureGuard } from '~/composables/useFeatureGuard'
const { hasAccess, isLoading } = useFeatureGuard('beta-access', '/upgrade')
</script>
<template>
<LoadingSpinner v-if="isLoading" />
<BetaContent v-else-if="hasAccess" />
<UpgradePrompt v-else />
</template>Parallel Data Fetching
<!-- pages/products.vue -->
<script setup lang="ts">
const { $supaship } = useNuxtApp()
// Fetch in parallel
const [products, features] = await Promise.all([
$fetch('/api/products'),
$supaship.getFeatures(['new-layout', 'show-banner']),
])
</script>
<template>
<div>
<Banner v-if="features['show-banner']" />
<ProductList :products="products" :layout="features['new-layout']" />
</div>
</template>Testing
Mock Plugin for Tests
// test-utils/setup.ts
import { createSupaship, FeaturesWithFallbacks } from '@supashiphq/vue-sdk'
export function createTestSupaship(features: FeaturesWithFallbacks) {
return createSupaship({
config: {
apiKey: 'test-key',
environment: 'test',
features,
context: {},
},
})
}Component Test
// components/FeatureCard.spec.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import { createTestSupaship } from '~/test-utils/setup'
import FeatureCard from './FeatureCard.vue'
describe('FeatureCard', () => {
it('shows feature when enabled', () => {
const wrapper = mount(FeatureCard, {
global: {
plugins: [
createTestSupaship({
'new-feature': true,
}),
],
},
})
expect(wrapper.text()).toContain('New Feature')
})
})Best Practices
1. Separate Client and Server Usage
- Use
@supashiphq/vue-sdkfor client-side composables - Use
@supashiphq/js-sdkfor server plugins, API routes, and middleware
2. Centralize Feature Definitions
// composables/features.ts
export const FEATURE_FLAGS = {
'new-feature': false,
} satisfies FeaturesWithFallbacks
// Use in both client and server contexts3. Use Type Augmentation
Always use type augmentation for type safety:
declare module '@supashiphq/vue-sdk' {
interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
}4. Environment-Specific Configuration
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
supashipApiKey: process.env.NUXT_PUBLIC_SUPASHIP_API_KEY || '',
supashipEnvironment:
process.env.NUXT_PUBLIC_SUPASHIP_ENVIRONMENT || 'production',
},
},
})5. Reactive Context Updates
Update context reactively when user state changes:
watch(user, newUser => {
if (newUser) {
updateContext({
userId: newUser.id,
email: newUser.email,
})
}
})6. Handle Loading States
<script setup lang="ts">
const { feature, isLoading } = useFeature('my-feature')
</script>
<template>
<Skeleton v-if="isLoading" />
<FeatureContent v-else-if="feature" />
<FallbackContent v-else />
</template>Troubleshooting
Plugin Not Working
Solution: Ensure the plugin file is named correctly and placed in the plugins/ directory:
// ✅ Correct - plugins/supaship.client.ts
export default defineNuxtPlugin(nuxtApp => {
// ...
})
// ❌ Incorrect - wrong location or namingType Errors
Property 'my-feature' does not exist on type 'Features'Solution: Add type augmentation:
declare module '@supashiphq/vue-sdk' {
interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
}Environment Variables Not Working
Solution: Ensure variables are prefixed with NUXT_PUBLIC_ and restart the dev server:
# .env
NUXT_PUBLIC_SUPASHIP_API_KEY=your-keySSR Hydration Mismatch
Solution: Ensure feature flags are fetched on both server and client, or use client-only features:
<!-- Use ClientOnly for client-only features -->
<ClientOnly>
<FeatureComponent />
</ClientOnly>Middleware Not Running
Solution: Ensure middleware is properly exported and registered:
// middleware/feature-guard.ts
export default defineNuxtRouteMiddleware(async (to, from) => {
// Ensure async operations complete
})