Vanilla JavaScript Integration
⚡ 25 min readIntegrate Transcodes WebAuthn/Passkey authentication into your Vite Vanilla JavaScript project without any framework.
Prerequisites
- Vite Vanilla JavaScript project
- Transcodes project ID from Dashboard
- HTTPS environment (or
localhostfor development)
Quick Start
Add WebWorker Script
Add the Transcodes WebWorker script to your index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Transcodes WebWorker -->
<script
type="module"
src="https://cdn.transcodes.link/%VITE_TRANSCODES_PROJECT_ID%/webworker.js"
></script>
<title>My App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/main.js"></script>
</body>
</html>Vite automatically 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 userId = null;
// DOM Elements
const app = document.getElementById('app');
// Initialize
init();
async function init() {
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) {
userId = 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) {
userId = result.payload[0].user.id;
console.log('Login successful:', result.payload[0].user.email);
}
}
async function handleSignOut() {
await transcodes.token.signOut();
}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
init();
async function init() {
// 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
A reusable class for managing authentication:
class AuthManager {
constructor(projectId) {
this.projectId = projectId;
this.isAuthenticated = false;
this.userId = null;
this.subscriptions = [];
this.listeners = new Set();
}
async init() {
// 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.userId = 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.userId = result.payload[0].user.id;
return result.payload[0].user;
}
return null;
}
async signOut() {
await transcodes.token.signOut();
this.userId = null;
}
async getUser() {
if (!this.userId) return null;
const result = await transcodes.user.get({
projectId: this.projectId,
userId: this.userId,
});
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';
// Initialize
await auth.init();
// Check auth state
if (auth.isAuthenticated) {
const user = await auth.getUser();
console.log('Logged in as:', user.email);
}
// Listen for changes
auth.onAuthChange((isAuthenticated) => {
console.log('Auth changed:', isAuthenticated);
render();
});
// Login
document.getElementById('login-btn').addEventListener('click', async () => {
const user = await auth.login();
if (user) {
console.log('Welcome,', user.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