---
title: Next.js
description: SSR auth for Next.js with HttpOnly cookie sessions.
sidebar:
  icon: triangle
  order: 3
---

`@vortex-api/convex-auth/nextjs` and `@vortex-api/convex-auth/nextjs/server` are the Next.js SSR adapter. Sessions live in app-origin HttpOnly cookies; middleware lands OAuth/magic-link redirects, refreshes sessions near expiry, and proxies auth actions at `/api/auth`. It implements the [SSR auth contract](/convex-auth/ssr-contract).

A working example lives in `examples/nextjs`.

## Install

```bash
pnpm add @vortex-api/convex-auth convex
```

Point the app at your Convex deployment and wire the backend (component mount, `convex/auth.ts`, `convex/http.ts`) per [Installation](/convex-auth/installation):

```bash
echo 'NEXT_PUBLIC_CONVEX_URL=https://<your-deployment>.convex.cloud' >> .env.local
```

## Middleware

Create `middleware.ts` (or `proxy.ts` on Next.js 16+) at the project root:

```ts
// middleware.ts
import {
  convexAuthNextjsMiddleware,
  createRouteMatcher,
  nextjsMiddlewareRedirect,
} from "@vortex-api/convex-auth/nextjs/server";
import { api } from "./convex/_generated/api";

const isProtectedRoute = createRouteMatcher(["/dashboard(.*)"]);

export default convexAuthNextjsMiddleware(
  async (request, ctx) => {
    if (isProtectedRoute(request)) {
      // Cheap pre-filter: no Convex call if the session cookie is absent.
      const { hasSessionCookie } = ctx.convexAuth.cookieState();
      if (!hasSessionCookie) {
        return nextjsMiddlewareRedirect(request, "/sign-in");
      }
      // Verified check (revocation-aware) for the protected route.
      if (!(await ctx.convexAuth.isAuthenticated())) {
        return nextjsMiddlewareRedirect(request, "/sign-in");
      }
    }
  },
  { actions: api.auth },
);

export const config = {
  // Only skip static assets — /api/auth must reach the middleware, which is
  // where the auth proxy intercepts it.
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
```

The middleware context exposes `ctx.convexAuth.getToken()`, `ctx.convexAuth.isAuthenticated()` (revocation-aware), and `ctx.convexAuth.cookieState()` (cheap cookie-presence/expiry check — revocation-blind, so use it for pre-filtering only).

### Options

| Option                   | Default                  | Description                                                                                                                                                             |
| ------------------------ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `actions`                | required                 | The `api.auth` object re-exporting `convexAuth` actions.                                                                                                                |
| `convexUrl`              | `NEXT_PUBLIC_CONVEX_URL` | Convex deployment URL.                                                                                                                                                  |
| `apiRoute`               | `/api/auth`              | Path that proxies auth actions. Must match the `apiRoute` prop of `ConvexAuthNextjsServerProvider`.                                                                     |
| `cookieConfig.maxAge`    | `null` (session cookie)  | Cookie lifetime in seconds. Must be `null` or a positive number.                                                                                                        |
| `requireLandingVerifier` | `true`                   | Require OAuth/magic-link landings to carry a verifier param matching the cookie minted at flow start. Set `false` only for deployments that predate verifier threading. |
| `verbose`                | `false`                  | Debug logging.                                                                                                                                                          |

## Server provider

Wrap the app in `ConvexAuthNextjsServerProvider` inside the root layout. It resolves and verifies the session from HttpOnly cookies on every server render and seeds the client provider:

```tsx
// app/layout.tsx
import { ConvexAuthNextjsServerProvider } from "@vortex-api/convex-auth/nextjs/server";
import { api } from "../convex/_generated/api";
import { ConvexClientProvider } from "./providers";

export default function RootLayout(props: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ConvexClientProvider>
          <ConvexAuthNextjsServerProvider actions={api.auth}>
            {props.children}
          </ConvexAuthNextjsServerProvider>
        </ConvexClientProvider>
      </body>
    </html>
  );
}
```

`ConvexClientProvider` is a client component that owns the `ConvexReactClient` and renders `ConvexProvider` — it must wrap the auth provider. See `examples/nextjs/app/providers.tsx`.

## Server helpers

From `@vortex-api/convex-auth/nextjs/server`:

| Helper                                 | Returns                              | Notes                                                                                                                            |
| -------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `convexAuthNextjsSession({ actions })` | `{ user, sessionId } \| null`        | Verified session via the `verifySession` query — revocation-aware. Memoized per request.                                         |
| `isAuthenticatedNextjs({ actions })`   | `boolean`                            | Verified check for Server Actions and Route Handlers. Don't check auth in layouts — they don't stop nested pages from rendering. |
| `convexAuthNextjsToken()`              | `string \| undefined`                | Raw session token from the cookie jar.                                                                                           |
| `convexAuthNextjsCookieState(request)` | `{ hasSessionCookie, tokenExpired }` | Optimistic boundary check — revocation-blind, pre-filtering only.                                                                |

## Route guards are UX, not security

Middleware redirects and `cookieState()` checks shape navigation; they are not authorization. Always verify the session at the data boundary (`convexAuthNextjsSession`, `verifySession`, or `ctx.auth.getUserIdentity()` inside Convex functions).

## Error params

Rejected landings redirect to `/?error=landing_verifier_mismatch` (verifier cookie absent or mismatched) or `/?error=cross_origin` (credentialed cross-origin request). Surface these in your UI — see the [SSR auth contract](/convex-auth/ssr-contract) for the full semantics.

## Sign-in UI

Client components use the React client — sign-in calls mint the session through the `/api/auth` proxy into HttpOnly cookies:

```tsx
// app/sign-in/page.tsx
"use client";

import { FormEvent, useState } from "react";
import { useRouter } from "next/navigation";
import { useConvexAuthClient } from "@vortex-api/convex-auth/react";

export default function SignInPage() {
  const authClient = useConvexAuthClient();
  const router = useRouter();

  const [error, setError] = useState<string | null>(null);

  const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const data = new FormData(e.currentTarget);
    const result = await authClient.signIn.email({
      email: String(data.get("email")),
      password: String(data.get("password")),
    });
    if (result.error) {
      setError(result.error.message ?? "Sign in failed");
      return;
    }
    // 2FA-enrolled accounts resolve without a session — show the TOTP step.
    if (result.data?.twoFactorRedirect) return;
    router.push("/dashboard");
    router.refresh();
  };

  const onGithub = async () => {
    const result = await authClient.signIn.social({ provider: "github" });
    // social() returns the authorization URL — it does not navigate for you.
    if (result.data?.url) window.location.href = result.data.url;
  };

  return (
    <form onSubmit={onSubmit}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button type="submit">Sign in</button>
      <button type="button" onClick={onGithub}>
        Continue with GitHub
      </button>
      {error ? <p role="alert">{error}</p> : null}
    </form>
  );
}
```

The full form (sign-up, OAuth buttons, guest, passkey, 2FA step-up) is in `examples/nextjs/app/sign-in/page.tsx`. See the [React guide](/convex-auth/react) for the complete hooks API.

## Example app

`examples/nextjs` exercises the adapter end-to-end — HttpOnly cookie sessions, middleware refresh, the `/api/auth` proxy, and server-rendered session state. No token ever touches browser JavaScript.

```bash
cd examples/nextjs
pnpm install
pnpm dlx convex dev --dev-deployment local   # local backend on :3210
pnpm run dev
```

## Next steps

**[React hooks](/convex-auth/react)**

`useSession`, `useConvexAuthClient`, and the drop-in client.

**[SSR auth contract](/convex-auth/ssr-contract)**

The boundary semantics every SSR adapter implements.

**[Production](/convex-auth/production)**

Deploy, monitor, and rotate.

**[Example source](https://github.com/VortexNYC/convex-auth/tree/main/examples/nextjs)**

`examples/nextjs` on GitHub.
