Vue
A Vue SDK for Supaship that provides composables for feature flag management with full TypeScript type safety.
Requirements
- Vue 3.0+ (Composition API support)
- Use JavaScript SDK for Vue 2 (see JavaScript code examples)
Installation
pnpm add @supashiphq/vue-sdkQuick Start
// main.ts
import { createApp } from 'vue'
import { createSupaship, FeaturesWithFallbacks } from '@supashiphq/vue-sdk'
import App from './App.vue'
// Define your features with type safety
const FEATURE_FLAGS = {
'new-header': false,
'theme-config': { mode: 'dark' 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> {}
}
const supaship = createSupaship({
config: {
apiKey: 'your-api-key',
environment: 'production',
features: FEATURE_FLAGS,
context: {
userID: '123',
email: '[email protected]',
},
},
})
const app = createApp(App)
app.use(supaship)
app.mount('#app')Using the Composable
<script setup lang="ts">
import { useFeature } from '@supashiphq/vue-sdk'
const { feature: newHeader, isLoading } = useFeature('new-header')
</script>
<template>
<div v-if="isLoading">Loading...</div>
<NewHeader v-else-if="newHeader" />
<OldHeader v-else />
</template>Type-Safe Feature Flags
For full TypeScript type safety, define your features and augment the Features interface:
// lib/features.ts
import { FeaturesWithFallbacks, InferFeatures } from '@supashiphq/vue-sdk'
export const FEATURE_FLAGS = {
'new-header': false,
'theme-config': {
mode: 'dark' as 'dark' | 'light',
primaryColor: '#007bff',
showLogo: true,
},
'beta-features': [] as string[],
'disabled-feature': null,
} satisfies FeaturesWithFallbacks
// Type augmentation for global type safety, it is required
declare module '@supashiphq/vue-sdk' {
interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
}Now useFeature and useFeatures will have full type safety:
<script setup lang="ts">
import { useFeature } from '@supashiphq/vue-sdk'
// TypeScript knows 'new-header' is valid and feature is ComputedRef<boolean | null>
const { feature } = useFeature('new-header')
// TypeScript knows 'theme-config' returns the exact object shape
const { feature: config } = useFeature('theme-config')
// config is ComputedRef<{ mode: 'dark' | 'light', primaryColor: string, showLogo: boolean } | null>
// TypeScript will error on invalid feature names
const { feature: invalid } = useFeature('non-existent-feature') // ❌ Type error
</script>API Reference
For complete API documentation, see the Vue SDK API Reference.
Best Practices
1. Always Use satisfies for Feature Definitions
// ✅ Good - preserves literal types
const features = {
'dark-mode': false,
theme: { mode: 'light' as const, variant: 'compact' as const },
} satisfies FeaturesWithFallbacks
// ❌ Bad - loses literal types (don't use type annotation)
const features: FeaturesWithFallbacks = {
'dark-mode': false,
theme: { mode: 'light', variant: 'compact' }, // Types widened to string
}2. Centralize Feature Definitions
// ✅ Good - centralized feature definitions
// lib/features.ts
export const FEATURE_FLAGS = {
'new-header': false,
theme: { mode: 'light' as const },
'beta-features': [] as string[],
} satisfies FeaturesWithFallbacks
// ❌ Bad - scattered feature definitions
const config1 = { features: { 'feature-1': false } satisfies FeaturesWithFallbacks }
const config2 = { features: { 'feature-2': true } satisfies FeaturesWithFallbacks }3. Use Type Augmentation for Type Safety
// ✅ Good - type augmentation for global type safety
declare module '@supashiphq/vue-sdk' {
interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
}
// Now all useFeature calls are type-safe
const { feature } = useFeature('new-header') // ✅ TypeScript knows this is ComputedRef<boolean>
const { feature } = useFeature('invalid') // ❌ TypeScript error4. Use Context for User Targeting
// main.ts
import { createApp } from 'vue'
import { createSupaship } from '@supashiphq/vue-sdk'
const supaship = createSupaship({
config: {
apiKey: 'your-api-key',
features: FEATURE_FLAGS,
context: {
// Initial context - can be updated later with useFeatureContext
version: import.meta.env.VITE_APP_VERSION,
environment: import.meta.env.MODE,
},
},
})
const app = createApp(App)
app.use(supaship)
app.mount('#app')Then update context dynamically in your components:
<script setup lang="ts">
import { watch } from 'vue'
import { useFeatureContext } from '@supashiphq/vue-sdk'
import { useAuth } from './composables/auth'
const { updateContext } = useFeatureContext()
const { user } = useAuth()
// Update context when user changes
watch(
user,
newUser => {
if (newUser) {
updateContext({
userId: newUser.id,
email: newUser.email,
plan: newUser.plan,
})
}
},
{ immediate: true },
)
</script>5. Batch Feature Requests
<script setup lang="ts">
// ✅ Good - single API call
const { features } = useFeatures(['feature-1', 'feature-2', 'feature-3'])
// ❌ Less efficient - multiple API calls
const feature1 = useFeature('feature-1')
const feature2 = useFeature('feature-2')
const feature3 = useFeature('feature-3')
</script>6. Handle Loading States
<script setup lang="ts">
import { computed } from 'vue'
import { useUser } from './composables/user'
import { useFeatures } from '@supashiphq/vue-sdk'
const { user, isLoading: userLoading } = useUser()
const { features, isLoading: featuresLoading } = useFeatures(['user-specific-feature'], {
context: computed(() => ({ userId: user.value?.id })),
shouldFetch: computed(() => !userLoading.value && !!user.value),
})
const isLoading = computed(() => userLoading.value || featuresLoading.value)
</script>
<template>
<Skeleton v-if="isLoading" />
<SpecialContent v-else-if="features['user-specific-feature']" />
</template>7. Update Context Reactively
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useFeatureContext } from '@supashiphq/vue-sdk'
const { updateContext } = useFeatureContext()
const currentPage = ref('dashboard')
// Update context when navigation changes
watch(currentPage, newPage => {
updateContext({ currentPage: newPage })
})
</script>
<template>
<div>
<Navigation @page-change="page => (currentPage = page)" />
<PageContent :page="currentPage" />
</div>
</template>Framework Integration
Vite
// main.ts
import { createApp } from 'vue'
import { createSupaship, FeaturesWithFallbacks } from '@supashiphq/vue-sdk'
import App from './App.vue'
const FEATURE_FLAGS = {
'new-ui': false,
theme: { mode: 'light' as const },
} satisfies FeaturesWithFallbacks
const supaship = createSupaship({
config: {
apiKey: import.meta.env.VITE_SUPASHIP_API_KEY,
environment: import.meta.env.MODE,
features: FEATURE_FLAGS,
},
})
const app = createApp(App)
app.use(supaship)
app.mount('#app')Nuxt 3
// plugins/supaship.client.ts
import { defineNuxtPlugin } from '#app'
import { createSupaship, FeaturesWithFallbacks } from '@supashiphq/vue-sdk'
const FEATURE_FLAGS = {
'new-homepage': false,
'dark-mode': false,
} satisfies FeaturesWithFallbacks
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,
},
})
nuxtApp.vueApp.use(supaship)
})// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
supashipApiKey: process.env.NUXT_PUBLIC_SUPASHIP_API_KEY || '',
},
},
})Feature Flag Guards for Vue Router
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useClient } from '@supashiphq/vue-sdk'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/beta',
component: () => import('./views/BetaFeature.vue'),
meta: { requiresFeature: 'beta-access' },
},
],
})
router.beforeEach(async (to, from, next) => {
const featureFlag = to.meta.requiresFeature as string | undefined
if (featureFlag) {
try {
const client = useClient()
const feature = await client.getFeature(featureFlag)
if (!feature) {
// Feature is disabled, redirect
return next('/404')
}
} catch (error) {
console.error('Error checking feature flag:', error)
return next('/error')
}
}
next()
})
export default routerDevelopment Toolbar
The SDK includes a development toolbar for testing and debugging feature flags locally.
app.use(
createSupaship({
config: { ... },
toolbar: {
enabled: 'auto', // 'auto' | 'always' | 'never'
position: 'bottom-right', // 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
},
}),
)'auto': Shows toolbar in development environments only (default)'always': Always shows toolbar'never': Never shows toolbar
The toolbar allows you to:
- View all available feature flags
- Override feature values locally
- See feature value types and current values
- Clear local overrides
Testing
Mocking Feature Flags in Tests
The plugin approach makes testing straightforward - just install the plugin with test features:
// 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: {},
},
})
}Example Test with Vitest
// MyComponent.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import { createTestSupaship } from '../test-utils/setup'
import MyComponent from './MyComponent.vue'
describe('MyComponent', () => {
it('shows new feature when enabled', () => {
const wrapper = mount(MyComponent, {
global: {
plugins: [
createTestSupaship({
'new-feature': true,
}),
],
},
})
expect(wrapper.text()).toContain('New Feature Content')
})
it('shows old feature when disabled', () => {
const wrapper = mount(MyComponent, {
global: {
plugins: [
createTestSupaship({
'new-feature': false,
}),
],
},
})
expect(wrapper.text()).toContain('Old Feature Content')
})
it('handles multiple features', () => {
const wrapper = mount(MyComponent, {
global: {
plugins: [
createTestSupaship({
'feature-a': true,
'feature-b': false,
config: { theme: 'dark' },
}),
],
},
})
expect(wrapper.find('.feature-a').exists()).toBe(true)
expect(wrapper.find('.feature-b').exists()).toBe(false)
})
})Troubleshooting
Common Issues
Type errors with FeaturesWithFallbacks
If you encounter type errors when defining features, ensure you’re using the correct pattern:
Solution: Always use satisfies FeaturesWithFallbacks (not type annotation)
// ✅ Good - preserves literal types
const features = {
'my-feature': false,
config: { theme: 'dark' as const },
} satisfies FeaturesWithFallbacks
// ❌ Bad - loses literal types
const features: FeaturesWithFallbacks = {
'my-feature': false,
config: { theme: 'dark' }, // Widened to string
}Plugin Not Installed Error
Error: useFeature must be used within a component tree that has the Supaship plugin installedSolution: Ensure your app has the plugin installed in main.ts:
// ✅ Correct - main.ts
import { createApp } from 'vue'
import { createSupaship } from '@supashiphq/vue-sdk'
import App from './App.vue'
const app = createApp(App)
app.use(createSupaship({ config: { ... } })) // Plugin installed
app.mount('#app')
// ❌ Incorrect - plugin not installed or installed after mount
const app = createApp(App)
app.mount('#app') // Plugin missing!Features Not Loading
- Check API key: Verify your API key is correct
- Check network: Open browser dev tools and check network requests
- Check features config: Ensure features are defined in the config
Type Errors
Property 'my-feature' does not exist on type 'Features'Solution: Add type augmentation:
import { InferFeatures } from '@supashiphq/vue-sdk'
import { FEATURE_FLAGS } from './features'
declare module '@supashiphq/vue-sdk' {
interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
}