Next.js integration for authentication. Provides cookie-backed session management (login, logout, getLoggedInUser, requireUser), composable API route handler factories for every auth flow (login, signup, OAuth, password reset, OTP), client-side React hooks, and a Next.js-specific Supabase auth client. Built on top of @repo/auth.
graph TD
auth_next["@repo/auth-next"]
analytics_core["@repo/analytics-core"]
auth["@repo/auth"]
db["@repo/db"]
errors["@repo/errors"]
logger["@repo/logger"]
safe["@repo/safe"]
typescript_config["@repo/typescript-config"]
vitest_config["@repo/vitest-config"]
auth_next --> analytics_core
auth_next --> auth
auth_next --> db
auth_next --> errors
auth_next --> logger
auth_next --> safe
auth_next -.-> typescript_config
auth_next -.-> vitest_config| Import | Resolves to | Description |
|---|---|---|
@repo/auth-next |
src/index.ts |
Session helpers: login, logout, getLoggedInUser, requireUser, cookie helpers |
@repo/auth-next/api |
src/api/index.ts |
All request handler factories (login, signup, OAuth, password flows, OTP) |
@repo/auth-next/hooks |
src/hooks/index.ts |
Client-side React hooks for auth flows |
@repo/auth-next/clients/* |
src/auth-clients/*.auth-client.ts |
createSupabaseNextAuthClient |
@repo/auth-next/lib/* |
src/lib/*.ts |
Low-level session/cookie utilities |
@repo/auth-next/tests |
__tests__/index.ts |
Vitest mocks for all hooks and auth clients |
subgraph nextjs [Next.js Server] Routes["API Route Files"] Factories["Request Handler Factories"] Session["Session Layer (cookies)"] Layout["Layouts (requireUser)"] end
subgraph authPkg ["@repo/auth"] AuthClient["BaseAuthClient"] GetUser["getUserFromToken"] end
subgraph infra [Infrastructure] Supabase["Supabase Auth"] end
Hooks -->|"POST /api/auth/*"| Routes Pages --> Layout Layout --> Session Routes --> Factories Factories --> AuthClient Factories --> Session AuthClient --> Supabase Session -->|"read cookie"| GetUser GetUser --> DB["@repo/db"]
subgraph nextjs [Next.js Server] Routes["API Route Files"] Factories["Request Handler Factories"] Session["Session Layer (cookies)"] Layout["Layouts (requireUser)"] end
subgraph authPkg ["@repo/auth"] AuthClient["BaseAuthClient"] GetUser["getUserFromToken"] end
subgraph infra [Infrastructure] Supabase["Supabase Auth"] end
Hooks -->|"POST /api/auth/*"| Routes Pages --> Layout Layout --> Session Routes --> Factories Factories --> AuthClient Factories --> Session AuthClient --> Supabase Session -->|"read cookie"| GetUser GetUser --> DB["@repo/db"]
graph TD
subgraph browser [Browser]
Hooks["React Hooks"]
Pages["Pages / Components"]
end
subgraph nextjs [Next.js Server]
Routes["API Route Files"]
Factories["Request Handler Factories"]
Session["Session Layer (cookies)"]
Layout["Layouts (requireUser)"]
end
subgraph authPkg ["@repo/auth"]
AuthClient["BaseAuthClient"]
GetUser["getUserFromToken"]
end
subgraph infra [Infrastructure]
Supabase["Supabase Auth"]
end
Hooks -->|"POST /api/auth/*"| Routes
Pages --> Layout
Layout --> Session
Routes --> Factories
Factories --> AuthClient
Factories --> Session
AuthClient --> Supabase
Session -->|"read cookie"| GetUser
GetUser --> DB["@repo/db"]
Sessions are managed via three HTTP-only cookies:
| Cookie | Purpose |
|---|---|
access-token |
JWT access token from Supabase |
refresh-token |
Refresh token for silent re-auth |
user-state |
Session state: "authenticated" or "password-reset" |
import { login, logout, getLoggedInUser, requireUser } from "@repo/auth-next";
| Function | Description |
|---|---|
login(tokens, options?) |
Sets all three cookies. Optionally pass { userState: "password-reset" } |
logout() |
Deletes all three cookies |
getLoggedInUser() |
Reads the access token cookie, calls getUserFromToken. Returns User | null |
requireUser(options?) |
Calls getLoggedInUser() and checks user state. Redirects to /login (or options.redirectTo) if not authenticated |
Auth routes follow a three-step pattern:
// apps/web/lib/auth.client.ts
import { createSupabaseNextAuthClient } from "@repo/auth-next/clients/supabase-next";
export const authClient = createSupabaseNextAuthClient({
oauthRedirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth`,
passwordResetRedirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/reset-password`,
});
// apps/web/app/api/auth/login/route.ts
import { createLoginRequestHandler } from "@repo/auth-next/api";
import { authClient } from "~/lib/auth.client";
const loginRequestHandler = createLoginRequestHandler({
authClient,
});
export { loginRequestHandler as POST };
Each factory takes { authClient, logger?, handleError? } and returns a Next.js route handler. Export it as the appropriate HTTP method (POST, GET).
| Factory | HTTP | Zod Schema | Description |
|---|---|---|---|
createLoginRequestHandler |
POST | { email, password } |
Sign in with email/password, sets session cookies |
createSignupRequestHandler |
POST | { email, password } |
Register a new account |
createGoogleLoginRequestHandler |
POST | (none) | Initiate Google OAuth, returns { url } |
createOAuthCallbackRequestHandler |
GET | { code } (query) |
Exchange OAuth code for session, redirects to app |
createForgotPasswordRequestHandler |
POST | { email } |
Send password reset email |
createResetPasswordRequestHandler |
GET | { code } (query) |
Exchange reset code, sets cookies with 10-min expiry, redirects |
createUpdatePasswordRequestHandler |
POST | { password } |
Update password during reset flow, clears session |
createChangePasswordRequestHandler |
POST | { password } |
Change password for authenticated user |
createVerifyOTPRequestHandler |
POST | { email, token, type } |
Verify OTP code (e.g. recovery), sets session |
All factories validate input with Zod, call the appropriate BaseAuthClient method, manage cookies via the session layer, and return a NextResponse.
All hooks make axios requests to /api/auth/* routes and return Safe<T> results.
| Hook | Description |
|---|---|
useLoginWithEmailAndPassword |
{ login, isLoggingIn, error } |
useLoginWithGoogle |
{ loginWithGoogle, isLoggingIn, error } |
useSignupWithEmailAndPassword |
{ signup, isSigningUp, error } |
useForgotPassword |
{ forgotPassword, isLoading, error } |
useVerifyOTP |
{ verifyOTP, isLoading, error } |
useUpdatePassword |
{ updatePassword, isLoading, error } |
useChangePassword |
{ changePassword, isLoading, error } |
"use client";
import { useLoginWithEmailAndPassword } from "@repo/auth-next/hooks";
import { useRouter } from "next/navigation";
function LoginForm() {
const router = useRouter();
const { login, isLoggingIn, error } = useLoginWithEmailAndPassword();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const result = await login({
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (result.data) {
router.push("/");
}
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
{error && <p>{error}</p>}
<button type="submit" disabled={isLoggingIn}>
{isLoggingIn ? "Signing in..." : "Sign in"}
</button>
</form>
);
}
Use requireUser() in server-side layouts or pages. If the user is not authenticated, they are automatically redirected to /login.
// apps/web/app/(app)/layout.tsx
import { requireUser } from "@repo/auth-next";
export default async function AppLayout({ children }: React.PropsWithChildren) {
const user = await requireUser();
return (
<UserProvider user={user}>
{children}
</UserProvider>
);
}
For custom redirect targets:
const user = await requireUser({ redirectTo: "/signin" });
The package exports Vitest mocks for all hooks via @repo/auth-next/tests:
import "@repo/auth-next/tests";
import { mockUseLoginWithEmailAndPassword } from "@repo/auth-next/tests";
// Mocks are automatically wired up via vi.mock
mockUseLoginWithEmailAndPassword.mockReturnValue({
login: vi.fn(),
isLoggingIn: false,
error: null,
});
| Script | Description |
|---|---|
lint |
Run Biome checks |
check-types |
Typecheck with tsc --noEmit |
test |
Run Vitest with coverage |
test:watch |
Run Vitest in watch mode |