Skip to Content

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-sdk package

Installation

pnpm add @supashiphq/vue-sdk

Quick 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.0

Using 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-sdk for client-side composables
  • Use @supashiphq/js-sdk for 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 contexts

3. 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 naming

Type 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-key

SSR 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 })
Last updated on