@supashiphq/vue-sdk
v1.1.0
Published
Supaship SDK for Vue 3
Readme
Supaship Vue SDK
Supaship SDK for Vue 3 that provides composables for feature flag management with full TypeScript type safety.
Installation
npm install @supashiphq/vue-sdk
# or
yarn add @supashiphq/vue-sdk
# or
pnpm add @supashiphq/vue-sdkQuick Start
// main.ts
import { createApp } from 'vue'
import { createSupaship, FeaturesWithFallbacks, InferFeatures } 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-safe useFeature / useFeatures keys and value types
declare module '@supashiphq/vue-sdk' {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
}
const supaship = createSupaship({
config: {
sdkKey: 'your-sdk-key',
environment: 'production',
features: FEATURE_FLAGS,
context: {
userId: '123',
email: '[email protected]',
},
},
})
const app = createApp(App)
// Configure the Supaship plugin
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>API Reference
createSupaship
Creates a Vue plugin for Supaship. This is the standard Vue way to set up the SDK globally.
// main.ts
import { createApp } from 'vue'
import { createSupaship } from '@supashiphq/vue-sdk'
const supaship = createSupaship(options)
const app = createApp(App)
app.use(supaship)
app.mount('#app')Options:
| Option | Type | Required | Description |
| --------- | ---------------------- | -------- | ---------------------------- |
| config | SupashipClientConfig | Yes | Configuration for the client |
| toolbar | ToolbarConfig | No | Development toolbar settings |
Configuration Options:
const config = {
sdkKey: 'your-sdk-key',
environment: 'production',
features: {
// Required: define all feature flags with fallback values
'my-feature': false,
config: { theme: 'light' },
},
context: {
// Optional: targeting context
userId: 'user-123',
email: '[email protected]',
plan: 'premium',
},
// Hash sensitive context properties such as PII on the client before sending to Edge
sensitiveContextProperties: ['email', 'userID'],
networkConfig: {
// Optional: network settings
featuresAPIUrl: 'https://api.supashiphq.com/features',
retry: {
enabled: true,
maxAttempts: 3,
backoff: 1000,
},
requestTimeoutMs: 5000,
},
}Privacy note: set
sensitiveContextPropertiesto hash PII/sensitive context property values on the client before requests are sent to the Edge API.
Supported Feature Value Types:
| Type | Example | Description |
| --------- | ----------------------------------- | ------------------------- |
| boolean | false | Simple on/off flags |
| object | { theme: 'dark', showLogo: true } | Configuration objects |
| array | ['feature-a', 'feature-b'] | Lists of values |
| null | null | Disabled/unavailable flag |
Note: Strings and numbers are not supported as standalone feature values. Use objects instead:
{ value: 'string' }or{ value: 42 }.
useFeature Composable
Retrieves a single feature flag value with Vue reactivity and full TypeScript type safety.
const result = useFeature(featureName, options?)Parameters:
featureName: string- The feature flag keyoptions?: objectcontext?: Record<string, unknown>- Context override for this requestshouldFetch?: boolean- Whether to fetch the feature (default: true)
Return Value:
{
feature: ComputedRef<T>, // The feature value (typed based on your Features interface)
isLoading: ComputedRef<boolean>, // Loading state
isSuccess: ComputedRef<boolean>, // Success state
isError: ComputedRef<boolean>, // Error state
error: Ref<Error | null>, // Error object if failed
status: Ref<'idle' | 'loading' | 'success' | 'error'>,
refetch: () => Promise<void>, // Function to manually refetch
// ... other query state properties
}Examples:
<script setup lang="ts">
import { useFeature } from '@supashiphq/vue-sdk'
// Simple boolean feature
const { feature: isEnabled, isLoading } = useFeature('new-ui')
</script>
<template>
<Skeleton v-if="isLoading" />
<NewUI v-else-if="isEnabled" />
<OldUI v-else />
</template><script setup lang="ts">
import { useFeature } from '@supashiphq/vue-sdk'
// Object feature
const { feature: config } = useFeature('theme-config')
</script>
<template>
<div v-if="config" :class="config.theme">
<Logo v-if="config.showLogo" />
<div :style="{ color: config.primaryColor }">Content</div>
</div>
</template><script setup lang="ts">
import { computed } from 'vue'
import { useFeature } from '@supashiphq/vue-sdk'
import { useUser } from './composables/user'
const { user, isLoading: userLoading } = useUser()
// Only fetch when user is loaded
const { feature } = useFeature('user-specific-feature', {
context: computed(() => ({ userId: user.value?.id })),
shouldFetch: computed(() => !userLoading.value && !!user.value),
})
</script>
<template>
<SpecialContent v-if="feature" />
</template>useFeatures Composable
Retrieves multiple feature flags in a single request with type safety.
const result = useFeatures(featureNames, options?)Parameters:
featureNames: readonly string[]- Array of feature flag keysoptions?: objectcontext?: Record<string, unknown>- Context override for this requestshouldFetch?: boolean- Whether to fetch features (default: true)
Return Value:
{
features: ComputedRef<{ [key: string]: T }>, // Object with feature values (typed based on keys)
isLoading: ComputedRef<boolean>,
isSuccess: ComputedRef<boolean>,
isError: ComputedRef<boolean>,
error: Ref<Error | null>,
status: Ref<'idle' | 'loading' | 'success' | 'error'>,
refetch: () => Promise<void>,
// ... other query state properties
}Examples:
<script setup lang="ts">
import { useFeatures } from '@supashiphq/vue-sdk'
import { useUser } from './composables/user'
const { user } = useUser()
// Fetch multiple features at once (more efficient than multiple useFeature calls)
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>useFeatureContext Composable
Access and update the feature context within components.
const { updateContext, getContext } = useFeatureContext()Example:
<script setup lang="ts">
import { ref } from 'vue'
import { useFeatureContext } from '@supashiphq/vue-sdk'
const { updateContext } = useFeatureContext()
const user = ref(null)
const handleUserUpdate = newUser => {
user.value = newUser
// Update feature context when user changes
// This will trigger refetch of all features
updateContext({
userId: newUser.id,
plan: newUser.subscriptionPlan,
segment: newUser.segment,
})
}
</script>
<template>
<form @submit="handleUserUpdate">
<!-- User profile form -->
</form>
</template>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: {
sdkKey: 'your-sdk-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: {
sdkKey: import.meta.env.VITE_SUPASHIP_SDK_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: {
sdkKey: config.public.supashipSDKKey as string,
environment: process.env.NODE_ENV || 'production',
features: FEATURE_FLAGS,
},
})
nuxtApp.vueApp.use(supaship)
})// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
supashipSDKKey: process.env.NUXT_PUBLIC_SUPASHIP_SDK_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)true: Always shows toolbarfalse: 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
Unit testing in your app
Register the plugin on the test app with toolbar: false and the feature map you want for that case:
import { mount } from '@vue/test-utils'
import { createSupaship, FeaturesWithFallbacks } from '@supashiphq/vue-sdk'
import MyBanner from './MyBanner.vue'
const features = { 'show-banner': true } satisfies FeaturesWithFallbacks
const plugin = createSupaship({
config: { sdkKey: 'test', environment: 'test', features, context: {} },
toolbar: false,
})
it('renders the banner', () => {
const w = mount(MyBanner, { global: { plugins: [plugin] } })
expect(w.find('[data-testid="banner"]').exists()).toBe(true)
})useFeature / useFeatures work inside that tree. For plain functions that take a SupashipClient, use new SupashipClient(...) and mock fetch like the JavaScript SDK example if you want no network.
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 SDK key: Verify your SDK 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> {}
}License
This project is licensed under the MIT License - see the LICENSE file for details.
