React Integration
⚡ 15 min readIntegrate Transcodes WebAuthn/Passkey authentication into your Vite React 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 React 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 for the full comparison.
| Approach | Best for |
|---|---|
Script in index.html | PWA (Web App Kit), copy-paste Dashboard snippets |
npm @bigstrider/transcodes-sdk | Vite SPA without PWA, bundler-based apps |
PWA: Load webworker.js, manifest, and sw.js via HTML. The npm SDK alone does not enable installable PWA.
Installation
Option A: Script in index.html (default — PWA & Dashboard flow)
Add SDK Script
Choose your integration mode:
Authentication Toolkit Cluster only — passkey login and step-up MFA:
<script
type="module"
src="https://cdn.transcodes.link/%VITE_TRANSCODES_PROJECT_ID%/webworker.js"
></script>Web App Toolkit Cluster (PWA) — installable app with manifest and 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>Add sw.js (Service Worker) — download 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.tsx
Remove the Transcodes <script> from index.html if you added it earlier. Vite supports top-level await. Call init once before createRoot:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { init } from '@bigstrider/transcodes-sdk';
import App from './App';
import './index.css';
await init({ projectId: import.meta.env.VITE_TRANSCODES_PROJECT_ID });
// Optional: await init({ projectId: '...', customUserId: 'uid_xxx', debug: true });
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);Then import named exports in your components:
import {
openAuthLoginModal,
isAuthenticated,
getCurrentMember,
signOut,
on,
} from '@bigstrider/transcodes-sdk';TypeScript
Types ship with the npm package (lib/types/). You do not need a separate transcodes.d.ts.
Skip Option A when you use npm only (no webworker.js in HTML).
Auth Context
You can use the same AuthContext shape with CDN or npm. Below is an npm SDK example.
'use client'; // Next.js only
import { createContext, useEffect, useState, type ReactNode } from 'react';
import {
isAuthenticated as sdkIsAuthenticated,
openAuthLoginModal as sdkLogin,
openAuthConsoleModal as sdkConsole,
openAuthIdpModal as sdkIdp,
signOut as sdkSignOut,
on,
} from '@bigstrider/transcodes-sdk';
interface AuthContextValue {
isAuthenticated: boolean;
isLoading: boolean;
memberId: string | null;
openAuthLoginModal: () => Promise<void>;
openAuthConsoleModal: () => Promise<void>;
openAuthIdpModal: (params: {
resource: string;
action: 'create' | 'read' | 'update' | 'delete';
}) => Promise<void>;
signOut: () => Promise<void>;
}
export const AuthContext = createContext<AuthContextValue>({
isAuthenticated: false,
isLoading: true,
memberId: null,
openAuthLoginModal: async () => {},
openAuthConsoleModal: async () => {},
openAuthIdpModal: async () => {},
signOut: async () => {},
});
export function AuthProvider({ children }: { children: ReactNode }) {
const [isAuth, setIsAuth] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [memberId, setMemberId] = useState<string | null>(null);
useEffect(() => {
sdkIsAuthenticated().then((auth) => {
setIsAuth(auth);
setIsLoading(false);
});
const unsubscribe = on('AUTH_STATE_CHANGED', ({ isAuthenticated, member }) => {
setIsAuth(isAuthenticated);
setMemberId(member?.id ?? null);
});
return () => unsubscribe();
}, []);
const value: AuthContextValue = {
isAuthenticated: isAuth,
isLoading,
memberId,
openAuthLoginModal: async () => {
const result = await sdkLogin({ webhookNotification: false });
if (result.success) {
setMemberId(result.payload[0]?.member?.id ?? null);
setIsAuth(true);
}
},
openAuthConsoleModal: async () => { await sdkConsole(); },
openAuthIdpModal: async (params) => { await sdkIdp(params); },
signOut: async () => {
await sdkSignOut({ webhookNotification: false });
setIsAuth(false);
setMemberId(null);
},
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}With CDN, swap sdkIsAuthenticated() for transcodes.token.isAuthenticated(), sdkLogin() for transcodes.openAuthLoginModal(), and so on.
Components
Login Button
import { use, useState } from 'react';
import { AuthContext } from '../context/AuthContext';
export function LoginButton() {
const { openAuthLoginModal } = use(AuthContext);
const [loading, setLoading] = useState(false);
const handleLogin = async () => {
setLoading(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 {
setLoading(false);
}
};
return (
<button onClick={handleLogin} disabled={loading}>
{loading ? 'Loading...' : 'Login with Passkey'}
</button>
);
}Member Profile
import { use, useEffect, useState } from 'react';
import { getCurrentMember } from '@bigstrider/transcodes-sdk';
import { AuthContext } from '../context/AuthContext';
import type { Member } from '../../types/transcodes';
export function MemberProfile() {
const { isAuthenticated, signOut } = use(AuthContext);
const [member, setMember] = useState<Member | null>(null);
useEffect(() => {
if (!isAuthenticated) {
setMember(null);
return;
}
void getCurrentMember().then(setMember);
}, [isAuthenticated]);
if (!isAuthenticated || !member) {
return null;
}
return (
<div className='member-profile'>
<h3>{member.name || 'Member'}</h3>
<p>{member.email}</p>
<button onClick={signOut}>Sign Out</button>
</div>
);
}CDN (Option A): use await transcodes.token.getCurrentMember() instead of getCurrentMember() from npm.
Protected Route
A route guard that shows the login modal when the visitor is not authenticated:
import { use, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { AuthContext } from '../context/AuthContext';
interface ProtectedRouteProps {
children: React.ReactNode;
redirectTo?: string;
}
export function ProtectedRoute({
children,
redirectTo = '/',
}: ProtectedRouteProps) {
const { isAuthenticated, isLoading, openAuthLoginModal } = use(AuthContext);
const navigate = useNavigate();
const hasAttemptedLogin = useRef(false);
useEffect(() => {
if (isLoading || isAuthenticated || hasAttemptedLogin.current) return;
hasAttemptedLogin.current = true;
openAuthLoginModal()
.then((result) => {
if (!result.success) {
navigate(redirectTo, { replace: true });
}
})
.catch(() => {
navigate(redirectTo, { replace: true });
});
}, [isLoading, isAuthenticated, openAuthLoginModal, navigate, redirectTo]);
if (isLoading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return null;
}
return <>{children}</>;
}App Setup
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { HomePage } from './pages/HomePage';
import { DashboardPage } from './pages/DashboardPage';
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path='/' element={<HomePage />} />
<Route
path='/dashboard'
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
export default App;Custom Hook
A simpler hook for basic authentication needs:
import { useState, useEffect } from 'react';
import type { AuthStateChangedPayload } from '../../types/transcodes';
const projectId = import.meta.env.VITE_TRANSCODES_PROJECT_ID;
export function useAuth() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check initial auth state
transcodes.token.isAuthenticated().then((isAuth) => {
setIsAuthenticated(isAuth);
setLoading(false);
});
// Subscribe to auth changes
const unsubscribe = transcodes.on(
'AUTH_STATE_CHANGED',
(payload: AuthStateChangedPayload) => {
setIsAuthenticated(payload.isAuthenticated);
}
);
return () => unsubscribe();
}, []);
const login = async () => {
const result = await transcodes.openAuthLoginModal({
projectId,
});
return result;
};
const signOut = () => transcodes.token.signOut();
return {
isAuthenticated,
loading,
login,
signOut,
};
}API Calls with Token
const API_BASE_URL = import.meta.env.VITE_API_URL || 'https://api.example.com';
export async function fetchWithAuth(
endpoint: string,
options: RequestInit = {}
) {
const token = await transcodes.token.getAccessToken();
if (!token) {
throw new Error('Not authenticated');
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}Best Practices
Recommended patterns for using Transcodes in React applications
- Use Context for Global State: Manage auth state with React Context
- Subscribe to Events: Use
AUTH_STATE_CHANGEDto sync React state - Always Cleanup: Unsubscribe from events in useEffect cleanup
- Async Methods: Remember
isAuthenticated()andgetAccessToken()are async - Type Safety: Use the provided TypeScript definitions
Common Mistakes
// WRONG: isAuthenticated() is async
if (transcodes.token.isAuthenticated()) {
// This always runs! (Promise is truthy)
}
// CORRECT: use await
if (await transcodes.token.isAuthenticated()) {
// This correctly checks auth status
}Next Steps
- Next.js Integration - Server-side rendering with Next.js
- API Reference - Full API documentation
- Security Best Practices - Security guidelines