Vue.js Integration
⚡ 20 min readIntegrate 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
localhostfor development)
Choose how you load the SDK
See Quick Integration — Two Ways to Integrate.
| Approach | Best for |
|---|---|
Script in index.html | PWA (Web App Kit) |
npm @bigstrider/transcodes-sdk | Vite + Vue SPA without PWA |
PWA: Use the HTML script path. npm-only setup cannot replace the PWA CDN / manifest / sw.js flow.
Installation
Option A: Script in index.html (default — PWA & Dashboard flow)
Add WebWorker Script
Authentication Toolkit Cluster only:
<script
type="module"
src="https://cdn.transcodes.link/%VITE_TRANSCODES_PROJECT_ID%/webworker.js"
></script>Web App Toolkit Cluster (PWA) — add manifest and script, plus sw.js (Service Worker):
<link rel="manifest" href="https://cdn.transcodes.link/%VITE_TRANSCODES_PROJECT_ID%/manifest.json" />
<script
type="module"
src="https://cdn.transcodes.link/%VITE_TRANSCODES_PROJECT_ID%/webworker.js"
></script>Download sw.js from Transcodes Dashboard → Web App Cluster → Installation Guide. Place in public/sw.js so it is served at /sw.js (required for PWA installability)
Vite replaces %VITE_*% placeholders in index.html with environment variables
Set Environment Variables
VITE_TRANSCODES_PROJECT_ID=proj_abc123xyzAdd TypeScript Type Definitions
Download transcodes.d.ts from the Transcodes Dashboard and save as types/transcodes.d.ts
Then update your 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-sdkCall 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):
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.
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
<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
<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:
<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
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
<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
<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:
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,
};
});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
- Use Composables: Create reusable
useAuthcomposable for authentication logic - Reactive State: Use
refandcomputedfor reactive auth state - Navigation Guard: Use Vue Router’s
beforeEachfor async route protection - Always Cleanup: Unsubscribe from events in
onUnmounted - Async Methods: Remember
isAuthenticated()andgetAccessToken()are async - 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
- Vanilla JS Integration - Pure JavaScript guide
- API Reference - Full API documentation
- Security Best Practices - Security guidelines