Vanilla JavaScript Integration
⚡ 25 min readIntegrate Transcodes WebAuthn/Passkey authentication into your Vite Vanilla JavaScript project without any framework
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 Vanilla JavaScript project
- 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 bundle without PWA |
PWA: Use the HTML script path. npm-only setup cannot replace manifest + sw.js.
Quick Start
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_abc123xyzCreate Main JavaScript File
const PROJECT_ID = import.meta.env.VITE_TRANSCODES_PROJECT_ID;
// State
let isAuthenticated = false;
let memberId = null;
// DOM Elements
const app = document.getElementById('app');
// Start app (after HTML script loads transcodes)
startApp();
async function startApp() {
render();
// Check initial auth state (async!)
isAuthenticated = await transcodes.token.isAuthenticated();
render();
// Subscribe to auth state changes
transcodes.on('AUTH_STATE_CHANGED', (payload) => {
isAuthenticated = payload.isAuthenticated;
if (!isAuthenticated) {
memberId = null;
}
render();
});
}
function render() {
app.innerHTML = isAuthenticated ? renderAuthView() : renderGuestView();
attachEventListeners();
}
function renderGuestView() {
return `
<div class="card">
<h1>Welcome</h1>
<p>Sign in with your Passkey</p>
<button id="login-btn">Login with Passkey</button>
</div>
`;
}
function renderAuthView() {
return `
<div class="card">
<h2>You are logged in!</h2>
<button id="signout-btn">Sign Out</button>
</div>
`;
}
function attachEventListeners() {
const loginBtn = document.getElementById('login-btn');
const signoutBtn = document.getElementById('signout-btn');
if (loginBtn) {
loginBtn.addEventListener('click', handleLogin);
}
if (signoutBtn) {
signoutBtn.addEventListener('click', handleSignOut);
}
}
async function handleLogin() {
const result = await transcodes.openAuthLoginModal({
projectId: PROJECT_ID,
});
if (result.success && result.payload.length > 0) {
memberId = result.payload[0].member.id;
console.log('Login successful:', result.payload[0].member?.email);
}
}
async function handleSignOut() {
await transcodes.token.signOut();
}If you prefer the npm SDK (no script tag), continue to Option B below.
Option B: npm SDK
Install
npm install @bigstrider/transcodes-sdkRemove the Transcodes <script> from index.html (if you switch to npm)
Run app logic after init
import {
init,
openAuthLoginModal,
isAuthenticated,
getCurrentMember,
signOut,
on,
} from '@bigstrider/transcodes-sdk';
const PROJECT_ID = import.meta.env.VITE_TRANSCODES_PROJECT_ID;
async function bootstrap() {
await init({ projectId: PROJECT_ID });
// Optional: await init({ projectId: PROJECT_ID, customUserId: 'uid_xxx', debug: true })
await startApp();
}
bootstrap();Move your app bootstrap into startApp() so it runs after init resolves.
Skip Option A when you use npm only (no webworker.js in HTML).
Complete Example
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Passkey Auth Demo</title>
<link rel="stylesheet" href="/style.css" />
<!-- Transcodes SDK -->
<script
type="module"
src="https://cdn.transcodes.link/%VITE_TRANSCODES_PROJECT_ID%/webworker.js"
></script>
</head>
<body>
<div class="container">
<header>
<h1>My Application</h1>
<nav>
<a href="/">Home</a>
<a href="/dashboard.html">Dashboard</a>
</nav>
</header>
<main id="app">
<div class="card">
<p>Loading...</p>
</div>
</main>
</div>
<script type="module" src="/main.js"></script>
</body>
</html>Protected Pages
Client-Side Route Protection
const PROJECT_ID = import.meta.env.VITE_TRANSCODES_PROJECT_ID;
// Check authentication on page load
startDashboard();
async function startDashboard() {
// IMPORTANT: isAuthenticated() is async!
const isAuth = await transcodes.token.isAuthenticated();
if (!isAuth) {
// Show login modal
const result = await transcodes.openAuthLoginModal({
projectId: PROJECT_ID,
});
if (!result.success) {
// Redirect to home if login cancelled
window.location.href = '/';
return;
}
}
// User is authenticated, show protected content
document.getElementById('protected-content').style.display = 'block';
document.getElementById('loading').style.display = 'none';
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Dashboard</title>
<script
type="module"
src="https://cdn.transcodes.link/%VITE_TRANSCODES_PROJECT_ID%/webworker.js"
></script>
</head>
<body>
<div id="loading">Loading...</div>
<div id="protected-content" style="display: none;">
<h1>Dashboard</h1>
<p>Welcome to your dashboard!</p>
</div>
<script type="module" src="/dashboard.js"></script>
</body>
</html>API Calls with Token
For the fetchWithAuth utility, see React Integration - API Calls with Token. The implementation is framework-agnostic
Usage example:
async function loadUserData() {
try {
const data = await fetchWithAuth('/user/profile');
console.log('User data:', data);
} catch (error) {
console.error('Failed to load user data:', error);
}
}Event Handling
Available Events
// AUTH_STATE_CHANGED - Authentication state changed
const unsubAuth = transcodes.on('AUTH_STATE_CHANGED', (payload) => {
console.log('Authenticated:', payload.isAuthenticated);
console.log('Token expires at:', payload.expiresAt);
});
// TOKEN_REFRESHED - Access token was refreshed
const unsubRefresh = transcodes.on('TOKEN_REFRESHED', (payload) => {
console.log('Token refreshed, expires at:', new Date(payload.expiresAt));
});
// TOKEN_EXPIRED - Token expired and cannot be refreshed
const unsubExpired = transcodes.on('TOKEN_EXPIRED', (payload) => {
console.log('Token expired at:', new Date(payload.expiredAt));
// Prompt user to log in again
alert('Session expired. Please log in again.');
});
// ERROR - SDK error occurred
const unsubError = transcodes.on('ERROR', (payload) => {
console.error(`[${payload.context}] ${payload.code}: ${payload.message}`);
});Unsubscribing from Events
The on() method returns an unsubscribe function:
// Subscribe
const unsubscribe = transcodes.on('AUTH_STATE_CHANGED', handler);
// Later: unsubscribe
unsubscribe();Alternatively, use off() with the same function reference:
function myHandler(payload) {
console.log(payload);
}
// Subscribe
transcodes.on('AUTH_STATE_CHANGED', myHandler);
// Unsubscribe (same function reference required)
transcodes.off('AUTH_STATE_CHANGED', myHandler);Auth Manager Class
Reusable auth helper for the CDN path. With npm, swap transcodes.token.isAuthenticated() for isAuthenticated(), etc.
class AuthManager {
constructor(projectId) {
this.projectId = projectId;
this.isAuthenticated = false;
this.memberId = null;
this.subscriptions = [];
this.listeners = new Set();
}
async bootstrap() {
// Check initial state (async!)
this.isAuthenticated = await transcodes.token.isAuthenticated();
// Subscribe to events
this.subscriptions.push(
transcodes.on('AUTH_STATE_CHANGED', (payload) => {
this.isAuthenticated = payload.isAuthenticated;
if (!payload.isAuthenticated) {
this.memberId = null;
}
this.notifyListeners();
})
);
this.subscriptions.push(
transcodes.on('ERROR', (payload) => {
console.error('Auth error:', payload.code, payload.message);
})
);
}
async login() {
const result = await transcodes.openAuthLoginModal({
projectId: this.projectId,
});
if (result.success && result.payload.length > 0) {
this.memberId = result.payload[0].member.id;
return result.payload[0].member;
}
return null;
}
async signOut() {
await transcodes.token.signOut();
this.memberId = null;
}
async getMember() {
if (!this.memberId) return null;
const result = await transcodes.member.get({
projectId: this.projectId,
memberId: this.memberId,
});
if (result.success && result.payload.length > 0) {
return result.payload[0];
}
return null;
}
async getAccessToken() {
return transcodes.token.getAccessToken();
}
onAuthChange(callback) {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}
notifyListeners() {
this.listeners.forEach((callback) => {
callback(this.isAuthenticated);
});
}
destroy() {
this.subscriptions.forEach((unsubscribe) => unsubscribe());
this.subscriptions = [];
this.listeners.clear();
}
}
// Usage
const auth = new AuthManager(import.meta.env.VITE_TRANSCODES_PROJECT_ID);
export { auth };import { auth } from './auth.js';
// Bootstrap listeners
await auth.bootstrap();
// Check auth state
if (auth.isAuthenticated) {
const member = await auth.getMember();
console.log('Logged in as:', member?.email);
}
// Listen for changes
auth.onAuthChange((isAuthenticated) => {
console.log('Auth changed:', isAuthenticated);
render();
});
// Login
document.getElementById('login-btn').addEventListener('click', async () => {
const member = await auth.login();
if (member) {
console.log('Welcome,', member.email);
}
});
// Sign out
document.getElementById('signout-btn').addEventListener('click', () => {
auth.signOut();
});Browser Compatibility Check
function checkWebAuthnSupport() {
// Check WebAuthn support
if (!window.PublicKeyCredential) {
console.warn('WebAuthn is not supported in this browser');
document.getElementById('passkey-btn').style.display = 'none';
document.getElementById('compatibility-warning').style.display = 'block';
return false;
}
// Check platform authenticator availability (optional)
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().then(
(available) => {
if (!available) {
console.log(
'Platform authenticator not available, security key may be needed'
);
}
}
);
return true;
}
// Check on load
checkWebAuthnSupport();Best Practices
Recommended patterns for using Transcodes in Vanilla JavaScript applications
- Async Methods: Remember
isAuthenticated()andgetAccessToken()are async - always useawait - Cleanup: Unsubscribe from events when no longer needed to prevent memory leaks
- Error Handling: Subscribe to the
ERRORevent and wrap async calls in try-catch - signOut(): Use
signOut(), notlogout() - HTTPS: WebAuthn requires HTTPS (localhost is allowed for development)
Common Mistakes
See React Integration - Common Mistakes for common pitfalls including async isAuthenticated() and method names
Additional Vanilla JS note:
// WRONG: getAccessToken is sync
const token = transcodes.token.accessToken;
// CORRECT: getAccessToken is async
const token = await transcodes.token.getAccessToken();Next Steps
- API Reference - Full API documentation
- Events API - Event listener details
- Security Best Practices - Security guidelines