Next.js

Next.js integration uses middleware to refresh tokens on every request and the cookies() API from next/headers to apply auth cookies.

Middleware

The recommended approach is to call auth.refresh() in middleware.ts. This runs on every request before your pages render.

// middleware.ts
import { server } from "@robelest/convex-auth/server";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

const auth = server({ url: process.env.CONVEX_URL! });

export async function middleware(request: NextRequest) {
  const result = await auth.refresh(request);

  if (result.redirect) {
    return result.response;
  }

  const { cookies, token } = result;

  const response = NextResponse.next();

  // Apply auth cookies
  for (const cookie of cookies) {
    response.cookies.set(cookie.name, cookie.value, {
      path: cookie.path ?? "/",
      httpOnly: cookie.httpOnly,
      secure: cookie.secure,
      sameSite: cookie.sameSite as "lax" | "strict" | "none",
      maxAge: cookie.maxAge,
    });
  }

  // Pass token via header for server components
  if (token) {
    response.headers.set("x-convex-token", token);
  }

  return response;
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

Auth proxy route

Create app/api/auth/route.ts to handle client-side sign-in and sign-out:

// app/api/auth/route.ts
import { server } from "@robelest/convex-auth/server";

const auth = server({ url: process.env.CONVEX_URL! });

export async function POST(request: Request) {
  return auth.proxy(request);
}

Server component usage

In a server component or layout, read the token from cookies using next/headers and pass it into your app-owned client auth provider:

// app/layout.tsx
import { cookies } from "next/headers";
import { AuthProvider } from "./auth-provider";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const cookieStore = await cookies();
  const token = cookieStore.get("convex-auth-token")?.value ?? null;

  return (
    <html>
      <body>
        <AuthProvider token={token}>{children}</AuthProvider>
      </body>
    </html>
  );
}

AuthProvider is your client component that owns the Convex and auth clients:

// app/auth-provider.tsx
"use client";

import { useEffect, useMemo } from "react";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { client as createAuthClient } from "@robelest/convex-auth/browser";
import { ConvexAuthProvider } from "@robelest/convex-auth/react";

export function AuthProvider({
  token,
  children,
}: {
  token: string | null;
  children: React.ReactNode;
}) {
  const { convex, auth } = useMemo(() => {
    const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL!;
    const convexClient = new ConvexReactClient(convexUrl);
    const authClient = createAuthClient({
      convex: convexClient,
      proxyPath: "/api/auth",
      token,
    });
    return { convex: convexClient, auth: authClient };
  }, [token]);

  useEffect(() => () => auth.destroy(), [auth]);

  return (
    <ConvexProvider client={convex}>
      <ConvexAuthProvider auth={auth}>{children}</ConvexAuthProvider>
    </ConvexProvider>
  );
}

The token option seeds the synchronous boot: a non-empty token renders signed in on hydration, and null renders signed out without a loading flash. See SSR overview.