Get started
Next.js quickstart
Route handler, middleware, sign-in buttons and the verified session in the App Router.
Five files give a Next.js App Router app hosted sign-in, protected pages and a verified session. Install the package and set the environment variables first.
Shared configuration#
One server-only module reads the environment once; everything else imports it.
// lib/auth-config.ts — server-only. Import it from route handlers,
// middleware and Server Components; never from a Client Component.
import type { AuthConfig } from '@3een/auth';
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} is not set`);
return value;
}
export const authConfig: AuthConfig = {
// Browser-safe: identifies your environment and names the session cookie.
clientId: required('AUTH_CLIENT_ID'),
// Secret: redeems sign-in codes and calls the API. Server only.
apiKey: required('AUTH_API_KEY'),
// Your environment's authentication API origin.
apiUrl: required('AUTH_API_URL'),
// Where users land after signing in when no return path was requested.
redirectUri: required('NEXT_PUBLIC_AUTH_REDIRECT_URI'),
};Mount the auth routes#
handleAuth answers every route under /api/auth. The one you register in the dashboard is the callback, https://your-app.example/api/auth/callback: the hosted page returns there with a one-time code.
// app/api/auth/[...auth]/route.ts
//
// Mounts the SDK's routes under /api/auth:
// GET /api/auth/signin → redirects to your hosted sign-in page
// GET /api/auth/signup → redirects to your hosted sign-up page
// GET /api/auth/callback → redeems the one-time ?code=, sets the session cookies
// POST /api/auth/sign-in → embedded <SignIn /> (email + password)
// POST /api/auth/sign-in-mfa → embedded <SignIn /> TOTP step
// POST /api/auth/sign-up → email + password sign-up
// POST /api/auth/sign-out → revokes the session, clears the cookies
// GET /api/auth/user → the signed-in user, for useSession()
// GET /api/auth/organizations, POST /api/auth/switch-organization
import { handleAuth } from '@3een/auth/nextjs';
import { authConfig } from '@/lib/auth-config';
const handler = handleAuth(authConfig);
export { handler as GET, handler as POST };Protect pages with middleware#
// middleware.ts (project root)
//
// Verifies the session cookie against your environment's signing keys on
// every matched request. Signed-out visitors to a protected route are sent to
// /api/auth/signin?redirect_uri=<the page they asked for>.
import { authMiddleware } from '@3een/auth/nextjs';
export default authMiddleware({
clientId: process.env.AUTH_CLIENT_ID,
apiKey: process.env.AUTH_API_KEY, // used only to refresh an expiring session
apiUrl: process.env.AUTH_API_URL,
publicRoutes: ['/', '/pricing', '/api/auth/*', '/api/webhooks/*'],
});
export const config = {
// Everything except Next.js internals and static files.
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};Sign-in and account buttons#
'use client';
// components/site-header.tsx — a Client Component
import { SignInButton, SignUpButton, SignedIn, SignedOut, UserButton } from '@3een/auth/components';
export function SiteHeader() {
return (
<header>
<SignedOut>
{/* Links to /api/auth/signin, which redirects to your hosted page. */}
<SignInButton returnTo="/dashboard" />
<SignUpButton returnTo="/onboarding" />
</SignedOut>
<SignedIn>
<UserButton afterSignOutUrl="/" menuItems={[{ label: 'Account', href: '/account' }]} />
</SignedIn>
</header>
);
}Read the session#
In Server Components and route handlers, getAuth gives the verified claims and a has() check. It verifies the token against your environment's published keys, with no call to the API.
// app/billing/page.tsx — a Server Component
import { notFound, redirect } from 'next/navigation';
import { getAuth } from '@3een/auth/nextjs';
import { authConfig } from '@/lib/auth-config';
export default async function BillingPage() {
// Verified locally against the environment's published keys (no API call).
const auth = await getAuth(authConfig);
if (!auth) redirect('/api/auth/signin?redirect_uri=/billing');
// Roles and permissions come from the verified token.
if (!auth.has({ permission: 'billing:read' })) notFound();
return <h1>Billing for {auth.claims.email}</h1>;
}