Skip to Content

Vue

A Vue SDK for Supaship that provides composables for feature flag management with full TypeScript type safety.

Requirements

Installation

pnpm add @supashiphq/vue-sdk

Quick 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 error

4. 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 router

Development 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 installed

Solution: 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> {} }
Last updated on