Next.js Integration
⚡ 12 min readIntegrate Transcodes WebAuthn/Passkey authentication into your Next.js 15 application with App Router and TypeScript.
Prerequisites
- Next.js 15 with App Router
- TypeScript
- Transcodes project ID from Transcodes Console
- HTTPS environment (or
localhostfor development)
This guide covers App Router only. Next.js 15 is the recommended version.
Installation
Set Environment Variables
NEXT_PUBLIC_TRANSCODES_PROJECT_ID=proj_abc123xyzAdd SDK Script to Root Layout
import Script from 'next/script';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang='en'>
<head>
<Script
src={`https://cdn.transcodes.link/${process.env.NEXT_PUBLIC_TRANSCODES_PROJECT_ID}/webworker.js`}
strategy='beforeInteractive'
/>
</head>
<body>{children}</body>
</html>
);
}Use strategy="beforeInteractive" to ensure the SDK loads before your app
hydrates.
Add TypeScript Type Definitions
Download the complete type definitions from the API Reference and save as types/transcodes.d.ts.
Then update your tsconfig.json:
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "types"]
}Auth Context (Client Component)
See React Integration - Auth Context for the full implementation. For Next.js, add 'use client' and use process.env:
'use client';
// Use process.env instead of import.meta.env
const projectId = process.env.NEXT_PUBLIC_TRANSCODES_PROJECT_ID!;
// ... rest is identical to React versionRoot Layout with Provider
import Script from 'next/script';
import { AuthProvider } from './providers/AuthProvider';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang='en'>
<head>
<Script
src={`https://cdn.transcodes.link/${process.env.NEXT_PUBLIC_TRANSCODES_PROJECT_ID}/webworker.js`}
strategy='beforeInteractive'
/>
</head>
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}Components
For LoginButton and UserProfile components, see React Integration - Components. In Next.js, add the 'use client' directive at the top and adjust imports:
'use client';
import { AuthContext } from '../providers/AuthProvider'; // Next.js pathProtected Route Component
See React Integration - Protected Route for the full implementation. For Next.js, use next/navigation:
'use client';
import { useRouter } from 'next/navigation'; // instead of react-router-dom
// Use router.push(redirectTo) instead of navigate(redirectTo, { replace: true })
// ... rest is identical to React versionProtected Page Example
import { ProtectedRoute } from '../components/ProtectedRoute';
import { DashboardContent } from './DashboardContent';
export default function DashboardPage() {
return (
<ProtectedRoute>
<DashboardContent />
</ProtectedRoute>
);
}'use client';
import { use } from 'react';
import { AuthContext } from '../providers/AuthProvider';
import { UserProfile } from '../components/UserProfile';
export function DashboardContent() {
const { signOut } = use(AuthContext);
return (
<div>
<h1>Dashboard</h1>
<UserProfile />
<button onClick={signOut}>Sign Out</button>
</div>
);
}API Route with Token Validation
Validate Transcodes tokens in your API routes:
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const authHeader = request.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const token = authHeader.split(' ')[1];
try {
// Validate token with your backend or Transcodes API
// The token is a JWT signed by Transcodes
return NextResponse.json({
message: 'Protected data',
// Return protected data
});
} catch (error) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
}
}Client-Side API Call
For the fetchWithAuth utility, see React Integration - API Calls with Token.
SSR Considerations
The Transcodes SDK runs client-side only. It requires browser APIs (IndexedDB, WebAuthn) that are not available during server-side rendering.
Hydration Handling
Use the isLoading state to prevent hydration mismatches:
'use client';
import { use } from 'react';
import { AuthContext } from '../providers/AuthProvider';
export function AuthStatus() {
const { isAuthenticated, isLoading } = use(AuthContext);
// Prevent hydration mismatch
if (isLoading) {
return <div>Loading...</div>;
}
return <div>{isAuthenticated ? 'Logged in' : 'Not logged in'}</div>;
}Dynamic Import (Alternative)
For components that should never render on server:
import dynamic from 'next/dynamic';
const AuthButton = dynamic(
() => import('./components/LoginButton').then((mod) => mod.LoginButton),
{ ssr: false }
);
export default function Page() {
return (
<div>
<h1>Welcome</h1>
<AuthButton />
</div>
);
}Custom Hook
For a simpler useAuth hook, see React Integration - Custom Hook. In Next.js, add 'use client' and use process.env.NEXT_PUBLIC_TRANSCODES_PROJECT_ID:
'use client';
const projectId = process.env.NEXT_PUBLIC_TRANSCODES_PROJECT_ID!;
// ... rest is identical to React versionBest Practices
Recommended patterns for using Transcodes in Next.js applications.
- Use
'use client'directive: Auth-related components must be Client Components - Handle Loading State: Use
isLoadingto prevent hydration mismatches - 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 - Environment Variables: Use
NEXT_PUBLIC_prefix for client-side access
Common Mistakes
See React Integration - Common Mistakes for common pitfalls including async isAuthenticated(), event name casing, and method names.
Next Steps
- React Integration - Vite React SPA guide
- Vue Integration - Vue.js guide
- API Reference - Full API documentation
- Security Best Practices - Security guidelines