Skip to content
v3.0.1 — MCP OAuth, organizations, passkeys, SSR adaptersChangelog
Convex Auth
Esc
↑↓navigate↵open⌘Jpreview
On this page

Next.js

SSR auth for Next.js with HttpOnly cookie sessions.

@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.

A working example lives in examples/nextjs.

Install

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:

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:

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

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

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

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

Next steps

Was this page helpful?