Next.js SDK
The @passkeybase/nextjs package provides full-stack passkey authentication for Next.js App Router applications, including client-side hooks, server-side session access, middleware, and API route protection.
Installation
npm install @passkeybase/nextjs @passkeybase/jsSet up your environment variables:
# .env.local
NEXT_PUBLIC_PASSKEYBASE_CLIENT_ID=pk_live_abc123
NEXT_PUBLIC_PASSKEYBASE_DOMAIN=myapp.auth.ing
PASSKEYBASE_DOMAIN=myapp.auth.ing
PASSKEYBASE_API_KEY=sk_live_secret_keyProvider Setup
Create a client component that wraps your app with PasskeyBaseProvider, then use it in your root layout.
// app/providers.tsx
'use client';
import { PasskeyBaseProvider } from '@passkeybase/nextjs';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<PasskeyBaseProvider
clientId={process.env.NEXT_PUBLIC_PASSKEYBASE_CLIENT_ID!}
domain={process.env.NEXT_PUBLIC_PASSKEYBASE_DOMAIN!}
redirectUri={
typeof window !== 'undefined'
? window.location.origin + '/callback'
: undefined
}
>
{children}
</PasskeyBaseProvider>
);
}// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Auth Hooks (Client)
useAuth() and useUser()
Use these hooks in client components ('use client') for auth state, login, logout, and user profile access.
// app/dashboard/page.tsx
'use client';
import { useAuth, useUser } from '@passkeybase/nextjs';
export default function DashboardPage() {
const { isAuthenticated, isLoading, logout, getAccessToken } = useAuth();
const { user } = useUser();
if (isLoading) return <div>Loading...</div>;
if (!isAuthenticated) return <div>Not authenticated</div>;
const fetchData = async () => {
const token = await getAccessToken();
const res = await fetch('/api/data', {
headers: { Authorization: `Bearer ${token}` },
});
return res.json();
};
return (
<div>
<h1>Welcome, {user?.displayName}</h1>
<p>{user?.email}</p>
<button onClick={fetchData}>Fetch data</button>
<button onClick={() => logout()}>Log out</button>
</div>
);
}getSession() (Server)
Access the session in Server Components, Route Handlers, and Server Actions.
// app/profile/page.tsx
import { getSession } from '@passkeybase/nextjs/server';
import { redirect } from 'next/navigation';
export default async function ProfilePage() {
const session = await getSession();
if (!session) {
redirect('/');
}
return (
<div>
<h1>{session.user.displayName}</h1>
<p>{session.user.email}</p>
<p>User ID: {session.user.sub}</p>
</div>
);
}// app/api/me/route.ts
import { getSession } from '@passkeybase/nextjs/server';
import { NextResponse } from 'next/server';
export async function GET() {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
return NextResponse.json({
user: session.user,
accessToken: session.accessToken,
});
}Protected Routes
withPasskeyBase Middleware
Use withPasskeyBase middleware to protect routes at the edge. Unauthenticated users are automatically redirected to the login page.
// middleware.ts
import { withPasskeyBase } from '@passkeybase/nextjs/server';
export default withPasskeyBase({
// Routes that require authentication
protectedRoutes: ['/dashboard', '/dashboard/:path*', '/settings'],
// Route to redirect unauthenticated users to
loginRoute: '/',
// Route to redirect authenticated users away from
publicOnlyRoutes: ['/login'],
});
export const config = {
matcher: ['/dashboard/:path*', '/settings', '/login'],
};withApiAuth
Wrap API Route Handlers with withApiAuth to require authentication and access the session.
// app/api/protected/route.ts
import { withApiAuth } from '@passkeybase/nextjs/server';
import { NextResponse } from 'next/server';
export const GET = withApiAuth(async (request, { session }) => {
// session is guaranteed to exist here
const userId = session.user.sub;
return NextResponse.json({
message: `Hello, ${session.user.displayName}`,
userId,
});
});
// Responds with 401 if no valid session existsLogin and Logout
// app/components/AuthButtons.tsx
'use client';
import { useAuth } from '@passkeybase/nextjs';
export function LoginButton() {
const { loginWithRedirect, isLoading } = useAuth();
return (
<button
onClick={() => loginWithRedirect()}
disabled={isLoading}
>
Sign in with Passkey
</button>
);
}
export function LogoutButton() {
const { logout } = useAuth();
return (
<button onClick={() => logout({ returnTo: window.location.origin })}>
Sign out
</button>
);
}Handling the Callback
Create a callback page to complete the OAuth redirect flow.
// app/callback/page.tsx
'use client';
import { useAuth } from '@passkeybase/nextjs';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
export default function CallbackPage() {
const { handleRedirectCallback } = useAuth();
const router = useRouter();
useEffect(() => {
const handle = async () => {
const result = await handleRedirectCallback();
const returnTo = result.state || '/dashboard';
router.push(returnTo);
};
handle();
}, []);
return <div>Completing login...</div>;
}Passkey Enrollment
Enrollment works on the client using the useEnrollment() hook. Generate enrollment tokens on the server with the @passkeybase/node SDK.
// app/settings/passkeys/page.tsx
'use client';
import { useAuth } from '@passkeybase/nextjs';
import { useEnrollment } from '@passkeybase/nextjs';
import { useState } from 'react';
export default function PasskeySettings() {
const { getAccessToken } = useAuth();
const { enrollPasskey, isEnrolling, error } = useEnrollment();
const handleEnroll = async () => {
const token = await getAccessToken();
const res = await fetch('/api/enrollment-token', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
});
const { enrollmentToken } = await res.json();
await enrollPasskey(enrollmentToken);
alert('Passkey added successfully!');
};
return (
<div>
<h2>Passkey Management</h2>
<button onClick={handleEnroll} disabled={isEnrolling}>
{isEnrolling ? 'Enrolling...' : 'Add a new passkey'}
</button>
{error && <p className="text-red-600">{error.message}</p>}
</div>
);
}// app/api/enrollment-token/route.ts
import { withApiAuth } from '@passkeybase/nextjs/server';
import { ManagementClient } from '@passkeybase/node';
import { NextResponse } from 'next/server';
const mgmt = new ManagementClient({
domain: process.env.PASSKEYBASE_DOMAIN!,
apiKey: process.env.PASSKEYBASE_API_KEY!,
});
export const POST = withApiAuth(async (request, { session }) => {
const token = await mgmt.createEnrollmentToken({
externalId: session.user.sub,
displayName: session.user.displayName,
expiresIn: 3600,
});
return NextResponse.json({ enrollmentToken: token });
});TypeScript Support
Both client hooks and server utilities are fully typed. The package exports types for sessions, users, and configuration.
import type { User, AuthResult } from '@passkeybase/js';
import type { Session } from '@passkeybase/nextjs/server';
// Client hooks are fully typed
const { user } = useUser(); // user: User | undefined
const { isAuthenticated } = useAuth(); // isAuthenticated: boolean
// Server session is typed
const session = await getSession(); // Session | null
interface Session {
user: User;
accessToken: string;
expiresAt: number;
}Next steps
- React SDK -- shared hooks and components
- Node.js SDK -- server-side Management API client
- JavaScript SDK -- core API reference