Skip to Content

Vanilla JavaScript Integration

⚡ 25 min read

Integrate 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 localhost for development)

Quick Start

Add WebWorker Script

Add the Transcodes WebWorker script to your index.html:

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

.env
VITE_TRANSCODES_PROJECT_ID=proj_abc123xyz

Create Main JavaScript File

main.js
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

index.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

dashboard.js
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'; }
dashboard.html
<!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:

auth.js
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 };
main.js
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.

  1. Async Methods: Remember isAuthenticated() and getAccessToken() are async - always use await
  2. Cleanup: Unsubscribe from events when no longer needed to prevent memory leaks
  3. Error Handling: Subscribe to the ERROR event and wrap async calls in try-catch
  4. signOut(): Use signOut(), not logout()
  5. 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

Last updated on