Setting up Next.js
Convex Auth supports server-side rendering (SSR) with the Next.js App Router. In an SSR app, sign-in, refresh, and sign-out run on your Next.js server rather than in the browser:
- The refresh token is stored in an
httpOnlycookie, so it never reaches client JavaScript. - The access token is available to both sides: Server Components use it to run authenticated Convex queries during rendering, and the browser uses it to authenticate the Convex client.
The sign-in method guides still apply. Your Convex backend and the method hooks you call from your UI are the same as in a single-page app. What changes is how the frontend connects to Convex Auth. This guide covers that wiring. Sign-in methods with Next.js covers the method-specific differences.
The code below comes from the Next.js example, which supports username + password and GitHub sign-in.
Setting up Convex Auth in a Next.js app
- Follow the “Getting Started” guide
Start from a Next.js App Router project. Then install Convex Auth and set up the core as described in the Getting Started guide.
- Set up the backend for your sign-in methods
Follow the backend steps of the guide for each sign-in method you want to use: install the components, add the
userstable, set up user creation, and export the public functions fromconvex/auth.ts.infoDon't follow the frontend steps (“Set up the provider on the client” and the steps that set up the authentication UI). The rest of this guide replaces them.
The SSR helpers need one more function from the core than a single-page app does. Export
isAuthenticatednext tosignOutandrefreshSession. The server uses it to check access tokens with your deployment, so it doesn't trust a cookie only because it can decode it.convex/auth.tsimport { components, internal } from "./_generated/api";import { setupConvexAuth } from "@convex-dev/auth/server";import { setupGithub } from "@convex-dev/auth/methods/github/server";import { setupUsernamePassword } from "@convex-dev/auth/methods/username-password/server";// The core owns sessions, accounts, and JWT minting. Each sign-in method is// wired to it with its own setup function, and only hands back its functions// once the app attaches the callbacks that create its user rows. Each method// exposes the functions its client hooks call, like// `signUpWithUsernamePassword` / `signInWithUsernamePassword` for the password// one.// Under SSR those calls get proxied to Convex via the SSR host. The sign-in// functions exposed here are wired up to be proxied in src/lib/serverAuth.ts.const auth = setupConvexAuth({ component: components.auth });export const { signOut, refreshSession, isAuthenticated } = auth;// `onSignIn` is optional, and runs on every sign-in including the first. This// app uses it to stamp the user's last sign-in.export const { signUpWithUsernamePassword, signInWithUsernamePassword } =setupUsernamePassword(auth, {component: components.authPassword,usernameComponent: components.authUsername,}).attachUserCallbacks({createUser: internal.users.createUser,onSignIn: internal.users.onSignIn,});export const { startSignInWithGithub, completeSignInWithGithub } = setupGithub(auth,{component: components.authGithub,allowedRedirectOrigins: ["http://localhost:3000"],},).attachUserCallbacks({createUser: internal.users.createUser,onSignIn: internal.users.onSignIn,}); - Set up the auth route handlers
Create
src/lib/serverAuth.tsand callsetupConvexAuthServer. It returns the handlers that sign users in, refresh the session, and sign them out.The
signInlist says which Convex functions the browser can call through the sign-in route. The route refuses any function that isn't listed. Add every function in your app that creates a session, like signing up or signing in. See Sign-in methods with Next.js for the functions each method needs.src/lib/serverAuth.tsimport { setupConvexAuthServer } from "@convex-dev/auth/ssr";import { api } from "@/convex/_generated/api";// The framework-agnostic auth handlers, configured once. The `secure` cookie// flag is decided here (HTTPS-only in production) and applies to every handler,// including sign-in. Route files under app/auth/ mount what this returns.export const auth = setupConvexAuthServer({convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,refreshSession: api.auth.refreshSession,signOut: api.auth.signOut,// Every function that signs a user in, meaning it creates a session and// returns its tokens, must be listed here. The sign-in route refuses// anything else.signIn: [api.auth.signInWithUsernamePassword,api.auth.signUpWithUsernamePassword,api.auth.completeSignInWithGithub,],cookieOptions: { secure: process.env.NODE_ENV === "production" },}); - Mount the auth routes
Mount each handler as a route under
app/auth/. The client posts to/auth/signin,/auth/refresh, and/auth/signout, so mount the handlers at exactly these paths.- signin
- refresh
- signout
app/auth/signin/route.tsimport { auth } from "@/src/lib/serverAuth";// The sign-in route. It serves every sign-in function listed in serverAuth.ts,// forwarding each call to the deployment. Instead of sending all resulting// tokens as part of the response body (as calling the Convex function directly// would do), it moves the minted refresh token into an httpOnly cookie on the// way back.//// Adding an auth method means adding its function to that allowlist. There is// no per-method route or client code.export const POST = auth.convexProxyHandler;app/auth/refresh/route.tsimport { auth } from "@/src/lib/serverAuth";export const POST = auth.refreshHandler;app/auth/signout/route.tsimport { auth } from "@/src/lib/serverAuth";export const POST = auth.signOutHandler; - Set up the Next.js helpers
Create
src/lib/convexAuth.tsxand callsetupConvexAuthNextjs. It returns the parts of the integration that are specific to Next.js:convexAuthNextjsProxybuilds the Next.js proxy (proxy.ts).nextjsProxyRedirectredirects a request from inside the proxy.convexAuthNextjsAccessTokenreads the access token in a Server Component.isAuthenticatedNextjschecks whether the current request is signed in.ConvexAuthNextjsServerProviderreplacesConvexAuthProvider.
src/lib/convexAuth.tsximport { setupConvexAuthNextjs } from "@convex-dev/auth/ssr/nextjs";import { api } from "@/convex/_generated/api";// The Next-specific server helpers: proxy (up-front refresh + redirects),// the Server-Component token accessor, and the provider that hydrates the// client. The sign-in / refresh / sign-out *route handlers* are mounted// separately under app/auth/ from the framework-agnostic handlers.export const {convexAuthNextjsProxy,nextjsProxyRedirect,convexAuthNextjsAccessToken,isAuthenticatedNextjs,ConvexAuthNextjsServerProvider,} = setupConvexAuthNextjs({convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,refreshSession: api.auth.refreshSession,isAuthenticated: api.auth.isAuthenticated,}); - Set up the proxy
Create
proxy.tsat the root of your app. The proxy runs before each page request. It refreshes the session if the access token is about to expire, so Server Components always see a current token. It also lets you redirect requests based on whether the user is signed in.warningKeep the
/authroutes out of thematcher. Those routes set their own cookies.proxy.tsimport {convexAuthNextjsProxy,nextjsProxyRedirect,} from "@/src/lib/convexAuth";// Uses the provided convexAuthNextjsProxy function to build a custom proxy handler// for this example application. As seen here, you supply a custom function that makes// routing decisions, which gets called with an `isAuthenticated` helper. That allows// checking to see if the browser request is currently authenticated and thus which// page should be rendered based on that state and the requested page.export default convexAuthNextjsProxy(async (request, { isAuthenticated }) => {const isAuthPage = ["/signin", "/signup"].includes(request.nextUrl.pathname);const authed = await isAuthenticated();if (!isAuthPage && !authed) {return nextjsProxyRedirect(request, "/signin");}if (isAuthPage && authed) {return nextjsProxyRedirect(request, "/");}});export const config = {// Run on everything except the /auth handlers (they own their own cookies)// and Next internals.matcher: ["/((?!auth(?:/|$)|_next/static|_next/image|favicon.ico).*)"],}; - Set up the provider
Wrap your app in
ConvexAuthNextjsServerProviderin the root layout. It is a Server Component that reads the access token from the cookie and renders the client-sideConvexAuthNextjsProvider, so the Convex client is ready to authenticate when the page loads. Use it in place ofConvexAuthProviderandConvexProvider.app/layout.tsximport { Suspense, type ReactNode } from "react";import { ConvexAuthNextjsServerProvider } from "@/src/lib/convexAuth";export const metadata = {title: "Convex Auth — Next.js SSR",};export default function RootLayout({ children }: { children: ReactNode }) {return (<html lang="en"><body style={{ fontFamily: "system-ui, sans-serif", margin: 0 }}>{/* The provider reads the auth cookies, so with Cache Components ithas to render inside a Suspense boundary. */}<Suspense><ConvexAuthNextjsServerProvider>{children}</ConvexAuthNextjsServerProvider></Suspense></body></html>);} - Render authenticated data on the server
In a Server Component, get the access token with
convexAuthNextjsAccessTokenand pass it topreloadQueryfromconvex/nextjs. The page renders with the signed-in user's data, and the client continues the query live withusePreloadedQuery.To decide on the server whether the user is signed in, use
isAuthenticatedNextjs()and not the presence of a token.convexAuthNextjsAccessTokenonly checks that the token hasn't expired.isAuthenticatedNextjschecks the token's signature with your deployment.app/page.tsximport { preloadQuery } from "convex/nextjs";import { api } from "@/convex/_generated/api";import { convexAuthNextjsAccessToken } from "@/src/lib/convexAuth";import { Dashboard } from "./Dashboard";// Server Component: read the access token from the cookie and preload the// authenticated query so the page renders signed-in on first paint (SSR).export default async function Home() {const token = await convexAuthNextjsAccessToken();const preloaded = await preloadQuery(api.users.loggedInUser,{},{ token: token ?? undefined },);return <Dashboard preloaded={preloaded} />;} - Use Convex Auth in Client Components
Import
useAuthActionsanduseAuthTokenfrom@convex-dev/auth/react, the same as in a single-page app. ImportuseConvexAuthand theAuthenticated,Unauthenticated, andAuthLoadingcomponents fromconvex/react. After you callsignOut, navigate away from the page so the proxy can apply your redirects.For your sign-in and sign-up pages, use the hooks of each sign-in method. See Sign-in methods with Next.js.
app/Dashboard.tsx"use client";import { useAuthActions, useAuthToken } from "@convex-dev/auth/react";import { Preloaded, usePreloadedQuery } from "convex/react";import { useRouter } from "next/navigation";import { api } from "@/convex/_generated/api";export function Dashboard({preloaded,}: {preloaded: Preloaded<typeof api.users.loggedInUser>;}) {const user = usePreloadedQuery(preloaded);const token = useAuthToken();const { signOut } = useAuthActions();const router = useRouter();return (<main style={{ padding: 24, maxWidth: 640 }}><h1>Next.js SSR + Convex Auth</h1><p>Signed in as <strong>{user?.name ?? "(loading)"}</strong> (user{" "}<code>{user?.id ?? "…"}</code>).</p><p style={{ color: "#666" }}>Access token (JS-readable):{" "}<code>{token?.slice(0, 24) ?? "none"}…</code><br />The refresh token is in an httpOnly cookie — it is never in JS.</p><buttononClick={async () => {await signOut();router.push("/signin");}}>Sign out</button></main>);}
Deploying
The auth cookies are marked Secure (sent only over HTTPS) when NODE_ENV is
production. That is the case for next build and next start, but not for
next dev, so the cookies also work on http://localhost.
The auth routes reject cross-site requests by comparing the request's Origin
with its Host. If your app runs behind a proxy that rewrites Host, add your
public origin to allowedOrigins in setupConvexAuthServer.