Next.js Integration API

Next.js 16 Proxy (middleware) and handler API reference.

Next.js 16 Proxy Integration

The Next.js integration provides zero-config authentication for Next.js 16+ applications using the new Proxy system (formerly Middleware). Import from @warpy-auth-sdk/core/next.

authMiddleware

Zero-config authentication proxy for Next.js 16 (Clerk-like ergonomics)

authMiddleware(configOrOptions?: AuthConfig | AuthMiddlewareOptions, maybeOptions?: AuthMiddlewareOptions): (request: NextRequest) => Promise<Response>

Parameters

configOrOptionsAuthConfig | AuthMiddlewareOptions

AuthConfig or middleware options (auto-detects from env if omitted)

maybeOptionsAuthMiddlewareOptions

Middleware options (when first param is AuthConfig)

Returns

(request: NextRequest) => Promise<Response>

Example

// proxy.ts (Next.js 16 Proxy file at app root)
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { authMiddleware } from '@warpy-auth-sdk/core/next';
import { google } from '@warpy-auth-sdk/core';

// With explicit config
const handler = authMiddleware(
  {
    secret: process.env.AUTH_SECRET!,
    provider: google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      redirectUri: process.env.GOOGLE_REDIRECT_URI!,
    }),
    callbacks: {
      async user(u) {
        // Resolve/upsert user from your DB
        return { id: '1', email: u.email, name: u.name, picture: u.picture };
      },
      jwt: (t) => t,
      session: (s) => s,
    },
  },
  {
    basePath: '/api/auth',
    successRedirect: '/dashboard',
    errorRedirect: '/login',
  }
);

export function proxy(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  if (pathname.startsWith('/api/auth')) {
    return handler(request);
  }
  return NextResponse.next();
}

export const config = {
  matcher: [
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    '/(api|trpc)(.*)',
  ],
};

createNextAuthHandler

Create Next.js route handler for manual route setup

createNextAuthHandler(config: AuthConfig, options?: NextAuthHandlerOptions): (request: Request) => Promise<Response>

Parameters

configAuthConfigrequired

Authentication configuration

optionsNextAuthHandlerOptions

Handler options

Returns

(request: Request) => Promise<Response>

Example

// app/api/auth/[...nextauth]/route.ts
import { createNextAuthHandler } from '@warpy-auth-sdk/core/next';
import { google } from '@warpy-auth-sdk/core';

const handler = createNextAuthHandler(
  {
    secret: process.env.AUTH_SECRET!,
    provider: google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      redirectUri: process.env.GOOGLE_REDIRECT_URI!,
    }),
  },
  {
    basePath: '/api/auth',
    successRedirect: '/dashboard',
    errorRedirect: '/login',
  }
);

export { handler as GET, handler as POST };

Configuration Options

AuthMiddlewareOptions

interface AuthMiddlewareOptions extends NextAuthHandlerOptions {
  // Base path for auth routes (default: "/auth")
  basePath?: string;

  // Redirect after successful auth (default: "/")
  successRedirect?: string;

  // Redirect on auth error (default: "/login")
  errorRedirect?: string;

  // Public routes (always accessible)
  publicRoutes?: string[];

  // Protected routes (require authentication)
  protectedRoutes?: string[];
}

NextAuthHandlerOptions

interface NextAuthHandlerOptions {
  // Base path for auth routes (default: "/auth")
  basePath?: string;

  // Redirect after successful auth (default: "/dashboard")
  successRedirect?: string;

  // Redirect on auth error (default: "/login")
  errorRedirect?: string;
}

Zero-Config Setup

The middleware can auto-detect configuration from environment variables:

# .env.local
AUTH_SECRET=your-secret-key-min-32-chars
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/callback/google
// proxy.ts (minimal setup)
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { authMiddleware } from '@warpy-auth-sdk/core/next';

// Zero-config: auto-detects from env
const handler = authMiddleware({
  basePath: '/api/auth',
  successRedirect: '/dashboard',
  errorRedirect: '/login',
});

export function proxy(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  if (pathname.startsWith('/api/auth')) {
    return handler(request);
  }
  return NextResponse.next();
}

export const config = {
  matcher: [
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    '/(api|trpc)(.*)',
  ],
};

Registered Routes

The middleware/handler automatically creates the following routes under basePath:

// Session management
GET  {basePath}/session              // Get current session JSON
POST {basePath}/signout              // Sign out user

// OAuth flows
GET  {basePath}/signin/:provider     // Start OAuth (redirects to provider)
GET  {basePath}/callback/:provider   // OAuth callback (sets session cookie)

// Email magic links
POST {basePath}/signin/email         // Send magic link
Body: { email: string, captchaToken?: string }

GET  {basePath}/callback/email       // Verify magic link token
Query: ?token=<jwt>&email=<email>

// Two-factor authentication
GET  {basePath}/signin/twofa         // Send/verify 2FA code
Query: ?email=<email> (send) or ?code=<code>&identifier=<id> (verify)

Complete Next.js 16 Setup

// proxy.ts import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { authMiddleware } from '@warpy-auth-sdk/core/next'; import { google, email } from '@warpy-auth-sdk/core'; import { PrismaClient } from '@prisma/client'; import { prismaAdapter } from '@warpy-auth-sdk/core'; const prisma = new PrismaClient(); const handler = authMiddleware( { secret: process.env.AUTH_SECRET!, provider: google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, redirectUri: process.env.GOOGLE_REDIRECT_URI!, }), adapter: prismaAdapter(prisma), callbacks: { async user(profile) { // Upsert user in database let user = await prisma.user.findUnique({ where: { email: profile.email }, }); if (!user) { user = await prisma.user.create({ data: { email: profile.email, name: profile.name, picture: profile.picture, }, }); } return user; }, jwt: (token) => { // Add custom claims token.customClaim = 'value'; return token; }, session: (session) => { // Customize session return session; }, }, captcha: { provider: { type: 'recaptcha-v3', secretKey: process.env.RECAPTCHA_SECRET_KEY!, }, enforce: { email: true, twofa: true, oauth: false, }, }, }, { basePath: '/api/auth', successRedirect: '/dashboard', errorRedirect: '/login', publicRoutes: ['/', '/about', '/pricing'], protectedRoutes: ['/dashboard/:path*', '/settings/:path*'], } ); export function proxy(request: NextRequest) { const pathname = request.nextUrl.pathname; // Handle auth routes if (pathname.startsWith('/api/auth')) { return handler(request); } // Let other requests pass through return NextResponse.next(); } export const config = { matcher: [ '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', '/(api|trpc)(.*)', ], };

Client-Side Usage

'use client'; import { AuthProvider, useAuth } from '@warpy-auth-sdk/core/hooks'; // Root layout export default function RootLayout({ children }) { return ( <html lang="en"> <body> <AuthProvider secret={process.env.NEXT_PUBLIC_AUTH_SECRET!}> {children} </AuthProvider> </body> </html> ); } // Login page function LoginPage() { const { signIn } = useAuth(); const [email, setEmail] = useState(''); return ( <div> <h1>Sign In</h1> {/* OAuth */} <a href="/api/auth/signin/google"> <button>Sign in with Google</button> </a> {/* Email magic link */} <form onSubmit={(e) => { e.preventDefault(); signIn(email); }}> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@example.com" /> <button type="submit">Send Magic Link</button> </form> </div> ); } // Protected page function DashboardPage() { const { session, loading, signOut } = useAuth(); if (loading) return <div>Loading...</div>; if (!session) return <LoginPage />; return ( <div> <h1>Dashboard</h1> <p>Welcome, {session.user.name}!</p> <button onClick={signOut}>Sign Out</button> </div> ); }

Server Component with Session

import { getServerSession } from '@warpy-auth-sdk/core/hooks/server'; import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; export default async function ServerPage() { // Get session in Server Component const cookieStore = await cookies(); const cookieHeader = cookieStore.toString(); const request = new Request('http://localhost', { headers: { cookie: cookieHeader }, }); const session = await getServerSession( request, process.env.AUTH_SECRET! ); // Redirect if not authenticated if (!session) { redirect('/login'); } // Fetch user data const userData = await fetch(`/api/users/${session.user.id}`); return ( <div> <h1>Server Component</h1> <p>User: {session.user.email}</p> <pre>{JSON.stringify(userData, null, 2)}</pre> </div> ); }

Next.js 16 Proxy

The proxy.ts file is Next.js 16's replacement for middleware.ts:

  • Always runs on Node runtime (no edge runtime support)
  • Supports all Node.js features (database connections, email sending, etc.)
  • Export a proxy function that returns a Response
  • Use matcher config to specify which routes to handle
  • Cannot use export const runtime (always Node)

Important Notes

  • The basePath must match your OAuth callback URLs
  • Email magic links require Node runtime for email sending
  • PKCE verifiers are stored in HttpOnly cookies automatically
  • OAuth CSRF uses in-memory state + cookie fallback for dev reloads
  • Session cookies are HttpOnly and Secure in production
  • The secret must be at least 32 characters for security

Advanced Configuration

Multiple Providers

To support multiple authentication providers, you'll need to handle provider selection in your UI and use separate route handlers or conditional logic:

// For multiple providers, use separate instances or conditional logic
// This is a limitation - consider using multiple route handlers instead

// app/api/auth/google/[...nextauth]/route.ts
const googleHandler = createNextAuthHandler({
  secret: process.env.AUTH_SECRET!,
  provider: google({ /* ... */ }),
});
export { googleHandler as GET, googleHandler as POST };

// app/api/auth/github/[...nextauth]/route.ts
const githubHandler = createNextAuthHandler({
  secret: process.env.AUTH_SECRET!,
  provider: github({ /* ... */ }),
});
export { githubHandler as GET, githubHandler as POST };

Custom Callbacks

const handler = authMiddleware({
  secret: process.env.AUTH_SECRET!,
  provider: google({ /* ... */ }),
  callbacks: {
    // Resolve user (called after OAuth or email verification)
    async user(profile, context) {
      console.log('Provider:', context?.provider);

      // Upsert user in your database
      const user = await db.user.upsert({
        where: { email: profile.email },
        create: {
          email: profile.email,
          name: profile.name,
          picture: profile.picture,
        },
        update: {
          name: profile.name,
          picture: profile.picture,
          lastLogin: new Date(),
        },
      });

      return user;
    },

    // Modify JWT claims before signing
    jwt(token) {
      token.role = 'admin';
      token.tenantId = 'tenant-123';
      return token;
    },

    // Modify session before returning to client
    session(session) {
      session.customField = 'value';
      return session;
    },
  },
});

CAPTCHA Protection

import { createCaptchaProvider } from '@warpy-auth-sdk/core/captcha';

const handler = authMiddleware({
  secret: process.env.AUTH_SECRET!,
  provider: email({ /* ... */ }),
  captcha: {
    provider: createCaptchaProvider({
      type: 'recaptcha-v3',
      secretKey: process.env.RECAPTCHA_SECRET_KEY!,
      minimumScore: 0.5, // reCAPTCHA v3 score threshold
    }),
    enforce: {
      email: true,   // Require CAPTCHA for email sign-in
      twofa: true,   // Require CAPTCHA for 2FA
      oauth: false,  // Don't require for OAuth (handled by provider)
    },
  },
});

Best Practices

  • Use environment variables for all secrets
  • Enable CAPTCHA for email and 2FA endpoints
  • Implement rate limiting for auth endpoints
  • Use database adapters for session persistence
  • Set appropriate session expiration times
  • Monitor authentication events and errors
  • Test OAuth flows in production-like environments
Next.js Integration API | @warpy-auth-sdk/core