Skip to main content
The Next.js module provides helpers for integrating Convex with Next.js features including Server Components, Server Actions, and Route Handlers.

Installation

npm install convex next react

Setup

All exported functions use the NEXT_PUBLIC_CONVEX_URL environment variable by default. The npx convex dev command automatically sets this during local development.
.env.local
NEXT_PUBLIC_CONVEX_URL=https://small-mouse-123.convex.cloud

Server Components

fetchQuery

Execute a query from a Server Component:
async function fetchQuery<Query extends FunctionReference<"query">>(
  query: Query,
  args?: Query["_args"],
  options?: NextjsOptions
): Promise<FunctionReturnType<Query>>
Usage:
import { fetchQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";

export default async function TasksPage() {
  const tasks = await fetchQuery(api.tasks.list, { completed: false });

  return (
    <div>
      {tasks.map((task) => (
        <div key={task._id}>{task.text}</div>
      ))}
    </div>
  );
}

fetchMutation

Execute a mutation from a Server Component or Server Action:
async function fetchMutation<Mutation extends FunctionReference<"mutation">>(
  mutation: Mutation,
  args?: Mutation["_args"],
  options?: NextjsOptions
): Promise<FunctionReturnType<Mutation>>
Usage in a Server Action:
import { fetchMutation } from "convex/nextjs";
import { api } from "@/convex/_generated/api";

export async function createTask(formData: FormData) {
  "use server";
  
  const text = formData.get("text") as string;
  await fetchMutation(api.tasks.create, { text });
}

fetchAction

Execute an action from a Server Component or Server Action:
async function fetchAction<Action extends FunctionReference<"action">>(
  action: Action,
  args?: Action["_args"],
  options?: NextjsOptions
): Promise<FunctionReturnType<Action>>
Usage:
import { fetchAction } from "convex/nextjs";
import { api } from "@/convex/_generated/api";

export default async function SummaryPage() {
  const summary = await fetchAction(api.ai.generateSummary, {
    text: "Some long text..."
  });

  return <div>{summary}</div>;
}

Preloading for Client Components

preloadQuery

Preload query data in a Server Component to pass to a Client Component:
async function preloadQuery<Query extends FunctionReference<"query">>(
  query: Query,
  args?: Query["_args"],
  options?: NextjsOptions
): Promise<Preloaded<Query>>
Server Component:
import { preloadQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";
import TaskList from "./TaskList";

export default async function TasksPage() {
  const preloadedTasks = await preloadQuery(api.tasks.list, {
    completed: false
  });

  return <TaskList preloadedTasks={preloadedTasks} />;
}
Client Component:
"use client";

import { Preloaded, usePreloadedQuery } from "convex/react";
import { api } from "@/convex/_generated/api";

export default function TaskList({
  preloadedTasks
}: {
  preloadedTasks: Preloaded<typeof api.tasks.list>;
}) {
  const tasks = usePreloadedQuery(preloadedTasks);

  return (
    <div>
      {tasks.map((task) => (
        <div key={task._id}>{task.text}</div>
      ))}
    </div>
  );
}

usePreloadedQuery

Use preloaded data in a Client Component and subscribe to updates:
import { usePreloadedQuery, Preloaded } from "convex/react";
import { api } from "@/convex/_generated/api";

function TaskList({ preloaded }: { 
  preloaded: Preloaded<typeof api.tasks.list> 
}) {
  const tasks = usePreloadedQuery(preloaded);
  // tasks will update reactively after initial render
  
  return (
    <div>
      {tasks.map((task) => <div key={task._id}>{task.text}</div>)}
    </div>
  );
}

Options

All functions accept a NextjsOptions object:
type NextjsOptions = {
  token?: string;
  url?: string;
  skipConvexDeploymentUrlCheck?: boolean;
};
Parameters:
  • token - JWT authentication token
  • url - Convex deployment URL (defaults to process.env.NEXT_PUBLIC_CONVEX_URL)
  • skipConvexDeploymentUrlCheck - Skip URL validation for self-hosted backends
Example with authentication:
import { auth } from "@/auth";
import { fetchQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";

export default async function ProfilePage() {
  const session = await auth();
  
  const profile = await fetchQuery(
    api.users.profile,
    {},
    { token: session?.token }
  );

  return <div>{profile.name}</div>;
}

Route Handlers

Use Convex functions in Next.js API routes:
import { fetchQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";
import { NextResponse } from "next/server";

export async function GET() {
  const tasks = await fetchQuery(api.tasks.list, {});
  return NextResponse.json(tasks);
}

export async function POST(request: Request) {
  const body = await request.json();
  const taskId = await fetchMutation(api.tasks.create, body);
  return NextResponse.json({ id: taskId });
}

Client-side usage

For client-side reactivity, use the standard React hooks with ConvexProvider: Layout or root component:
"use client";

import { ConvexProvider, ConvexReactClient } from "convex/react";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export function ConvexClientProvider({
  children
}: {
  children: React.ReactNode;
}) {
  return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}
Client Component:
"use client";

import { useQuery, useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";

export default function Tasks() {
  const tasks = useQuery(api.tasks.list, {});
  const createTask = useMutation(api.tasks.create);

  return (
    <div>
      {tasks?.map((task) => <div key={task._id}>{task.text}</div>)}
      <button onClick={() => createTask({ text: "New task" })}>
        Add Task
      </button>
    </div>
  );
}

Patterns

Server Component with Client Component

Combine server-side data fetching with client-side reactivity:
// app/page.tsx (Server Component)
import { preloadQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";
import TaskList from "./TaskList";

export default async function Home() {
  const preloadedTasks = await preloadQuery(api.tasks.list, {});
  return <TaskList preloaded={preloadedTasks} />;
}

// TaskList.tsx (Client Component)
"use client";
import { usePreloadedQuery, useMutation, Preloaded } from "convex/react";
import { api } from "@/convex/_generated/api";

export default function TaskList({
  preloaded
}: {
  preloaded: Preloaded<typeof api.tasks.list>;
}) {
  const tasks = usePreloadedQuery(preloaded);
  const createTask = useMutation(api.tasks.create);

  return (
    <div>
      {tasks.map((task) => <div key={task._id}>{task.text}</div>)}
      <button onClick={() => createTask({ text: "New" })}>
        Add
      </button>
    </div>
  );
}

Server Actions

"use client";

import { createTask } from "./actions";

export default function CreateTaskForm() {
  return (
    <form action={createTask}>
      <input name="text" />
      <button type="submit">Create</button>
    </form>
  );
}

// actions.ts
"use server";
import { fetchMutation } from "convex/nextjs";
import { api } from "@/convex/_generated/api";

export async function createTask(formData: FormData) {
  const text = formData.get("text") as string;
  await fetchMutation(api.tasks.create, { text });
}

Authentication

Pass auth tokens from your auth provider:
import { auth } from "@clerk/nextjs";
import { fetchQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";

export default async function ProtectedPage() {
  const { getToken } = auth();
  const token = await getToken({ template: "convex" });
  
  const data = await fetchQuery(
    api.users.getCurrentUser,
    {},
    { token }
  );

  return <div>{data.name}</div>;
}

Best practices

  • Use preloadQuery to hydrate Client Components with initial data
  • Execute mutations in Server Actions for progressive enhancement
  • Use fetchQuery for server-only pages that don’t need reactivity
  • Combine server and client rendering for optimal performance
  • Pass authentication tokens via the token option
  • Use environment variables for the Convex URL
  • Leverage Next.js caching strategies with fetchQuery

Type safety

All functions are fully type-safe with generated API types:
import { api } from "@/convex/_generated/api";

// Fully type-checked
const tasks = await fetchQuery(api.tasks.list, {
  completed: false // Type-checked
});
// tasks has type Task[]

const preloaded = await preloadQuery(api.tasks.list, {});
// preloaded has type Preloaded<typeof api.tasks.list>

Caching

Next.js caches fetchQuery results by default. Control caching with Next.js options:
import { fetchQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";

// Force dynamic (no caching)
export const dynamic = 'force-dynamic';

// Or use Next.js revalidation
export const revalidate = 60; // Revalidate every 60 seconds

export default async function Page() {
  const tasks = await fetchQuery(api.tasks.list, {});
  return <div>{tasks.length} tasks</div>;
}

Build docs developers (and LLMs) love