Complete guide to integrating @warpy-auth-sdk/core with Next.js App Router
This guide covers integrating @warpy-auth-sdk/core with Next.js App Router. The SDK provides multiple integration methods, from zero-config Proxy to custom Route Handlers.
The SDK offers three integration approaches for Next.js:
npm install @warpy-auth-sdk/core
# or
yarn add @warpy-auth-sdk/core
# or
pnpm add @warpy-auth-sdk/coreTypical Next.js App Router project structure with authentication:
my-app/
├── app/
│ ├── layout.tsx # Root layout with AuthProvider
│ ├── page.tsx # Public home page
│ ├── login/
│ │ └── page.tsx # Login page
│ ├── dashboard/
│ │ └── page.tsx # Protected dashboard
│ └── api/
│ └── auth/
│ ├── session/
│ │ └── route.ts # Session endpoint (if not using Proxy)
│ └── signin/
│ └── email/
│ └── route.ts # Email auth endpoint (if needed)
├── proxy.ts # Next.js 16 Proxy (recommended)
└── .env.local # Environment variablesCreate a .env.local file with your authentication credentials:
# Required
AUTH_SECRET=your-secret-key-at-least-32-characters-long
# Google OAuth (example)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/callback/google
# Email provider (optional, for magic links)
RESEND_API_KEY=your-resend-api-key
# or SMTP
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-passwordAUTH_SECRET must be at least 32 characters long and kept secret. Generate a secure random string for production.The simplest way to add authentication is using the Next.js 16 Proxy (formerly Middleware). This provides a Clerk-like ergonomic experience with automatic route handling.
Zero-config authentication proxy
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";
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: {
// Resolve/upsert user in your database
async user(oauthUser) {
// In production, save to database
return {
id: oauthUser.id || oauthUser.email,
email: oauthUser.email,
name: oauthUser.name,
picture: oauthUser.picture,
};
},
jwt: (token) => token,
session: (session) => session,
},
},
{
basePath: "/api/auth",
successRedirect: "/dashboard",
errorRedirect: "/login",
}
);
export function proxy(request: NextRequest) {
const p = request.nextUrl.pathname;
if (p.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)(.*)",
],
};The Proxy automatically creates these endpoints:
GET /api/auth/session - Get current sessionPOST /api/auth/signout - Sign out userGET /api/auth/signin/google - Start Google OAuthGET /api/auth/callback/google - OAuth callbackPOST /api/auth/signin/email - Send magic link (Node runtime)For more control, implement custom Route Handlers:
Get current session
import { getSession } from "@warpy-auth-sdk/core";
import { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
try {
const session = await getSession(request, process.env.AUTH_SECRET!);
return Response.json({ session });
} catch (error) {
return Response.json({ session: null }, { status: 401 });
}
}Start Google OAuth flow
import { authenticate } from "@warpy-auth-sdk/core";
import { google } from "@warpy-auth-sdk/core";
import { NextRequest, NextResponse } from "next/server";
const config = {
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!,
}),
};
export async function GET(request: NextRequest) {
try {
const result = await authenticate(config, request);
if (result.redirectUrl) {
return NextResponse.redirect(result.redirectUrl);
}
return Response.json({ error: "Authentication failed" }, { status: 400 });
} catch (error) {
console.error("Auth error:", error);
return NextResponse.redirect("/login?error=auth_failed");
}
}Handle OAuth callback
import { authenticate, createSessionCookie } from "@warpy-auth-sdk/core";
import { google } from "@warpy-auth-sdk/core";
import { NextRequest, NextResponse } from "next/server";
const config = {
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(oauthUser) {
// Save to database in production
return {
id: oauthUser.email,
email: oauthUser.email,
name: oauthUser.name,
picture: oauthUser.picture,
};
},
},
};
export async function GET(request: NextRequest) {
try {
const result = await authenticate(config, request);
if (result.session) {
const cookie = createSessionCookie(result.session, config.secret);
const response = NextResponse.redirect(
new URL("/dashboard", request.url)
);
response.headers.set("Set-Cookie", cookie);
return response;
}
return NextResponse.redirect("/login?error=auth_failed");
} catch (error) {
console.error("Callback error:", error);
return NextResponse.redirect("/login?error=callback_failed");
}
}Sign out user
import { signOut, clearSessionCookie } from "@warpy-auth-sdk/core";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
try {
await signOut(request, process.env.AUTH_SECRET!);
const response = Response.json({ success: true });
response.headers.set("Set-Cookie", clearSessionCookie());
return response;
} catch (error) {
return Response.json(
{ error: "Sign out failed" },
{ status: 500 }
);
}
}Wrap your app with the AuthProvider to enable session management:
Root layout with authentication
import { AuthProvider } from "@warpy-auth-sdk/core/hooks";
import "./globals.css";
export const metadata = {
title: "My App",
description: "App with authentication",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}Login page with OAuth
'use client';
import { useAuth } from "@warpy-auth-sdk/core/hooks";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function LoginPage() {
const { session, loading } = useAuth();
const router = useRouter();
useEffect(() => {
if (session) {
router.push("/dashboard");
}
}, [session, router]);
if (loading) {
return <div>Loading...</div>;
}
return (
<div className="min-h-screen flex items-center justify-center">
<div className="max-w-md w-full space-y-8">
<h2 className="text-center text-3xl font-bold">
Sign in to your account
</h2>
<a
href="/api/auth/signin/google"
className="w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
Continue with Google
</a>
</div>
</div>
);
}Protected page requiring authentication
'use client';
import { useAuth } from "@warpy-auth-sdk/core/hooks";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function DashboardPage() {
const { session, loading, signOut } = useAuth();
const router = useRouter();
useEffect(() => {
if (!loading && !session) {
router.push("/login");
}
}, [session, loading, router]);
if (loading) {
return <div>Loading...</div>;
}
if (!session) {
return null;
}
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="py-16">
<h1 className="text-3xl font-bold">
Welcome, {session.user.name}!
</h1>
<p className="mt-2 text-gray-600">
Email: {session.user.email}
</p>
<button
onClick={signOut}
className="mt-4 bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700"
>
Sign Out
</button>
</div>
</div>
</div>
);
}Access the session in Server Components using getServerSession:
Server component with session access
import { getServerSession } from "@warpy-auth-sdk/core/hooks/server";
import { redirect } from "next/navigation";
export default async function ProfilePage() {
const session = await getServerSession();
if (!session) {
redirect("/login");
}
return (
<div>
<h1>Profile</h1>
<p>Name: {session.user.name}</p>
<p>Email: {session.user.email}</p>
</div>
);
}Support multiple authentication providers:
import { authMiddleware } from "@warpy-auth-sdk/core/next";
import { google, github, email } from "@warpy-auth-sdk/core";
// Note: The SDK currently supports one provider per config
// For multiple providers, you'll need to handle routing logic
const handler = authMiddleware({
secret: process.env.AUTH_SECRET!,
provider: google({ /* config */ }),
callbacks: {
async user(u) { /* ... */ },
},
});The SDK includes full TypeScript support with type definitions:
import type {
AuthConfig,
Session,
User
} from "@warpy-auth-sdk/core";
// Session is fully typed
const { session } = useAuth();
// session.user.email - autocomplete works!Ensure cookies are enabled and the AUTH_SECRET is consistent:
// Check if session cookie is being set
const response = NextResponse.redirect("/dashboard");
response.headers.set("Set-Cookie", cookie);If your frontend is on a different domain, configure CORS:
export async function GET(request: NextRequest) {
const response = await fetch(/* ... */);
response.headers.set("Access-Control-Allow-Origin", "https://yourdomain.com");
response.headers.set("Access-Control-Allow-Credentials", "true");
return response;
}Ensure the redirect URI in your OAuth provider settings matches exactly:
# Must match in both .env.local and OAuth provider console
GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/callback/google