PasskeyBase

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

Terminal
npm install @passkeybase/nextjs @passkeybase/js

Set up your environment variables:

.env.local
# .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_key

Provider Setup

Create a client component that wraps your app with PasskeyBaseProvider, then use it in your root layout.

app/providers.tsx
// 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
// 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
// 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 (Server Component)
// 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 (Route Handler)
// 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
// 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
// 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 exists

Login and Logout

app/components/AuthButtons.tsx
// 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
// 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
// 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
// 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.

TypeScript types
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