Skip to Content

Vue.js Integration

⚡ 20 min read

Integrate Transcodes WebAuthn/Passkey authentication into your Vite Vue 3 application with TypeScript

Client-side rendering only. The Transcodes SDK runs exclusively in the browser. Never use it in server-side rendering (SSR) or server components.


Prerequisites

  • Vite Vue 3 project with TypeScript
  • Transcodes project ID from Dashboard 
  • HTTPS environment (or localhost for development)

Choose how you load the SDK

See Quick Integration — Two Ways to Integrate.

ApproachBest for
Script in index.htmlDashboard CDN snippets
npm @bigstrider/transcodes-sdkVite + Vue SPA

Installation

Option A: Script in index.html (default — Dashboard flow)

Add WebWorker Script

Authentication Toolkit Cluster only:

index.html
<script type="module" src="https://cdn.transcodes.link/%VITE_TRANSCODES_PROJECT_ID%/webworker.js" ></script>

Vite replaces %VITE_*% placeholders in index.html with environment variables

Set Environment Variables

.env
VITE_TRANSCODES_PROJECT_ID=proj_abc123xyz

Add TypeScript Type Definitions

Download transcodes.d.ts from the Transcodes Dashboard  and save as types/transcodes.d.ts

Then update your tsconfig.json:

tsconfig.json
{ "compilerOptions": { "typeRoots": ["./node_modules/@types", "./types"] }, "include": ["src", "types"] }

If you prefer the npm SDK (no script tag), continue to Option B below.


Option B: npm SDK

Install the package

npm install @bigstrider/transcodes-sdk

Call init in main.ts

Remove the Transcodes <script> from index.html if you added it earlier. Call init once before mounting the app (Vite supports top-level await):

src/main.ts
import { createApp } from 'vue'; import { init } from '@bigstrider/transcodes-sdk'; import App from './App.vue'; await init({ projectId: import.meta.env.VITE_TRANSCODES_PROJECT_ID }); // Optional: await init({ projectId: '...', customUserId: 'uid_xxx', debug: true }) createApp(App).mount('#app');

Then use named exports from components or composables:

import { openAuthLoginModal, isAuthenticated, getCurrentMember, signOut, on, off, } from '@bigstrider/transcodes-sdk';

TypeScript

Types ship with the npm package. Add types/transcodes.d.ts only if you use the CDN path (Option A).

Skip Option A when you use npm only (no webworker.js in HTML).


Composable (useAuth)

Vue 3 Composition API composable example for the npm SDK.

src/composables/useAuth.ts
import { ref, readonly, onMounted, onUnmounted } from 'vue'; import { isAuthenticated as sdkIsAuthenticated, openAuthLoginModal as sdkLogin, openAuthConsoleModal as sdkConsole, openAuthIdpModal as sdkIdp, signOut as sdkSignOut, on, } from '@bigstrider/transcodes-sdk'; // Shared state across component instances const isAuth = ref(false); const isLoading = ref(true); const memberId = ref<string | null>(null); export function useAuth() { let unsubscribe: (() => void) | null = null; onMounted(async () => { isAuth.value = await sdkIsAuthenticated(); isLoading.value = false; unsubscribe = on('AUTH_STATE_CHANGED', ({ isAuthenticated, member }) => { isAuth.value = isAuthenticated; memberId.value = member?.id ?? null; }); }); onUnmounted(() => unsubscribe?.()); const openAuthLoginModal = async () => { const result = await sdkLogin({ webhookNotification: false }); if (result.success && result.payload.length > 0) { memberId.value = result.payload[0].member?.id ?? null; isAuth.value = true; } return result; }; const openAuthConsoleModal = () => sdkConsole(); const openAuthIdpModal = (params: { resource: string; action: 'create' | 'read' | 'update' | 'delete'; }) => sdkIdp(params); const signOut = async () => { await sdkSignOut({ webhookNotification: false }); isAuth.value = false; memberId.value = null; }; return { isAuthenticated: readonly(isAuth), isLoading: readonly(isLoading), memberId: readonly(memberId), openAuthLoginModal, openAuthConsoleModal, openAuthIdpModal, signOut, }; }

With CDN, replace sdkIsAuthenticated() with transcodes.token.isAuthenticated(), sdkLogin() with transcodes.openAuthLoginModal(), and so on.

isAuthenticated() / sdkIsAuthenticated() are async. Always await them.


Components

Login Button

src/components/LoginButton.vue
<script setup lang="ts"> import { ref } from 'vue'; import { useAuth } from '../composables/useAuth'; const { openAuthLoginModal } = useAuth(); const loading = ref(false); const handleLogin = async () => { loading.value = true; try { const result = await openAuthLoginModal(); if (result.success) { console.log('Login successful:', result.payload[0].member?.email); } } catch (error) { console.error('Login error:', error); } finally { loading.value = false; } }; </script> <template> <button @click="handleLogin" :disabled="loading" class="login-btn"> {{ loading ? 'Loading...' : 'Login with Passkey' }} </button> </template> <style scoped> .login-btn { padding: 12px 24px; background: #000; color: #fff; border: none; border-radius: 8px; cursor: pointer; } .login-btn:disabled { opacity: 0.5; cursor: not-allowed; } </style>

Member Profile

src/components/MemberProfile.vue
<script setup lang="ts"> import { ref, watch } from 'vue'; import { getCurrentMember } from '@bigstrider/transcodes-sdk'; import { useAuth } from '../composables/useAuth'; import type { Member } from '../../types/transcodes'; const { isAuthenticated, signOut } = useAuth(); const member = ref<Member | null>(null); watch( isAuthenticated, async (authenticated) => { if (authenticated) { try { member.value = await getCurrentMember(); } catch (error) { console.error('Failed to get member:', error); } } else { member.value = null; } }, { immediate: true } ); </script> <template> <div v-if="isAuthenticated && member" class="member-profile"> <div class="info"> <h3>{{ member.name || 'Member' }}</h3> <p>{{ member.email }}</p> </div> <button @click="signOut" class="signout-btn">Sign Out</button> </div> </template> <style scoped> .member-profile { display: flex; align-items: center; gap: 16px; padding: 16px; background: #f5f5f5; border-radius: 8px; } .info h3 { margin: 0; } .info p { margin: 4px 0 0; color: #666; } .signout-btn { margin-left: auto; padding: 8px 16px; background: transparent; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; } </style>

CDN (Option A): use await transcodes.token.getCurrentMember() instead of importing getCurrentMember from npm.

Auth Guard Component

A component that shows the login modal when the visitor is not authenticated:

src/components/AuthGuard.vue
<script setup lang="ts"> import { watch, ref } from 'vue'; import { useRouter } from 'vue-router'; import { useAuth } from '../composables/useAuth'; const props = defineProps<{ redirectTo?: string; }>(); const { isAuthenticated, isLoading, openAuthLoginModal } = useAuth(); const router = useRouter(); const hasAttemptedLogin = ref(false); watch( [isAuthenticated, isLoading], async ([authenticated, loading]) => { if (loading || authenticated || hasAttemptedLogin.value) return; hasAttemptedLogin.value = true; try { const result = await openAuthLoginModal(); if (!result.success) { router.push(props.redirectTo || '/'); } } catch { router.push(props.redirectTo || '/'); } }, { immediate: true } ); </script> <template> <div v-if="isLoading" class="loading">Loading...</div> <slot v-else-if="isAuthenticated" /> </template> <style scoped> .loading { display: flex; justify-content: center; align-items: center; min-height: 200px; } </style>

Router Setup

Vue Router with Navigation Guards

src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router'; const routes: RouteRecordRaw[] = [ { path: '/', name: 'Home', component: () => import('../views/HomeView.vue'), }, { path: '/dashboard', name: 'Dashboard', component: () => import('../views/DashboardView.vue'), meta: { requiresAuth: true }, }, { path: '/settings', name: 'Settings', component: () => import('../views/SettingsView.vue'), meta: { requiresAuth: true }, }, ]; const router = createRouter({ history: createWebHistory(), routes, }); // Navigation Guard router.beforeEach(async (to, _from, next) => { const requiresAuth = to.matched.some((record) => record.meta.requiresAuth); if (requiresAuth) { // Check authentication status (async!) const isAuth = await transcodes.token.isAuthenticated(); if (isAuth) { next(); } else { // Store intended destination and redirect to home next({ name: 'Home', query: { redirect: to.fullPath } }); } } else { next(); } }); export default router;

Important: The navigation guard uses await transcodes.token.isAuthenticated() because it’s an async method


App.vue Example

src/App.vue
<script setup lang="ts"> import { useAuth } from './composables/useAuth'; import LoginButton from './components/LoginButton.vue'; import MemberProfile from './components/MemberProfile.vue'; const { isAuthenticated, isLoading } = useAuth(); </script> <template> <div id="app"> <header> <nav> <router-link to="/">Home</router-link> <router-link to="/dashboard">Dashboard</router-link> </nav> <div class="auth-section"> <div v-if="isLoading">Loading...</div> <MemberProfile v-else-if="isAuthenticated" /> <LoginButton v-else /> </div> </header> <main> <router-view /> </main> </div> </template> <style> #app { max-width: 1200px; margin: 0 auto; padding: 20px; } header { display: flex; justify-content: space-between; align-items: center; padding: 20px 0; border-bottom: 1px solid #eee; } nav { display: flex; gap: 20px; } nav a { text-decoration: none; color: #333; } nav a.router-link-active { font-weight: bold; } </style>

Protected View Example

src/views/DashboardView.vue
<script setup lang="ts"> import AuthGuard from '../components/AuthGuard.vue'; import MemberProfile from '../components/MemberProfile.vue'; </script> <template> <AuthGuard redirect-to="/"> <div class="dashboard"> <h1>Dashboard</h1> <MemberProfile /> </div> </AuthGuard> </template>

Pinia Store (Alternative)

For applications using Pinia for state management:

src/stores/auth.ts
import { defineStore } from 'pinia'; import { ref } from 'vue'; import type { ApiResponse, AuthResult, AuthStateChangedPayload, Member, } from '../../types/transcodes'; const projectId = import.meta.env.VITE_TRANSCODES_PROJECT_ID; export const useAuthStore = defineStore('auth', () => { const isAuthenticated = ref(false); const isLoading = ref(true); const memberId = ref<string | null>(null); const member = ref<Member | null>(null); let unsubscribe: (() => void) | null = null; async function bootstrapAuth() { // Check initial auth state isAuthenticated.value = await transcodes.token.isAuthenticated(); isLoading.value = false; // Subscribe to auth changes unsubscribe = transcodes.on( 'AUTH_STATE_CHANGED', (payload: AuthStateChangedPayload) => { isAuthenticated.value = payload.isAuthenticated; if (!payload.isAuthenticated) { memberId.value = null; member.value = null; } } ); } async function login(): Promise<ApiResponse<AuthResult[]>> { const result = await transcodes.openAuthLoginModal({ projectId, }); if (result.success && result.payload.length > 0) { memberId.value = result.payload[0].member.id ?? null; member.value = result.payload[0].member; } return result; } async function signOut() { await transcodes.token.signOut(); memberId.value = null; member.value = null; } function cleanup() { if (unsubscribe) { unsubscribe(); } } return { isAuthenticated, isLoading, memberId, member, bootstrapAuth, login, signOut, cleanup, }; });
src/main.ts
import { createApp } from 'vue'; import { createPinia } from 'pinia'; import App from './App.vue'; import router from './router'; import { useAuthStore } from './stores/auth'; const app = createApp(App); const pinia = createPinia(); app.use(pinia); app.use(router); // Bootstrap auth listeners (after Transcodes script or npm init) const authStore = useAuthStore(); authStore.bootstrapAuth(); app.mount('#app');

If you use the npm SDK together with Pinia, call await init({ projectId }) before createApp / bootstrapAuth()—combine this block with the Option B main.ts pattern above.


API Calls with Token

For the fetchWithAuth utility, see React Integration - API Calls with Token. The implementation is framework-agnostic


Best Practices

Recommended patterns for using Transcodes in Vue applications

  1. Use Composables: Create reusable useAuth composable for authentication logic
  2. Reactive State: Use ref and computed for reactive auth state
  3. Navigation Guard: Use Vue Router’s beforeEach for async route protection
  4. Always Cleanup: Unsubscribe from events in onUnmounted
  5. Async Methods: Remember isAuthenticated() and getAccessToken() are async
  6. Type Safety: Use the provided TypeScript definitions

Common Mistakes

See React Integration - Common Mistakes for common pitfalls including async isAuthenticated(), event name casing, and method names


Next Steps

Last updated on