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.tstype-checked

// 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.tstype-checked

// 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.tstype-checked

// 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#

components/site-header.tsxtype-checked

'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.tsxtype-checked

// 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>;
}