Next.js with Convex DB Integration

A comprehensive guide to integrating Convex DB with Next.js, including type-safe queries and mutations


Modern database integration using Convex DB with Next.js, featuring type-safe queries, mutations, and real-time updates.

Schema Definition

schema.ts
import { defineSchema, defineTable } from "convex/schema";
import { v } from "convex/values";
 
export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
    role: v.union(v.literal("USER"), v.literal("ADMIN")),
    createdAt: v.number(),
  }).index("by_email", ["email"]),
 
  posts: defineTable({
    title: v.string(),
    content: v.string(),
    authorId: v.id("users"),
    published: v.boolean(),
    tags: v.array(v.string()),
    createdAt: v.number(),
  })
    .index("by_author", ["authorId"])
    .index("by_published", ["published"]),
});

Types

types.ts
import { Doc, Id } from "./_generated/dataModel";
 
export type User = Doc<"users">;
export type Post = Doc<"posts">;
export type UserId = Id<"users">;
export type PostId = Id<"posts">;
 
export type CreateUserInput = {
  name: string;
  email: string;
  role?: "USER" | "ADMIN";
};
 
export type CreatePostInput = {
  title: string;
  content: string;
  authorId: UserId;
  tags?: string[];
};

Queries

queries.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
 
export const getUsers = query({
  args: {
    role: v.optional(v.union(v.literal("USER"), v.literal("ADMIN"))),
  },
  handler: async (ctx, args) => {
    const users = await ctx.db
      .query("users")
      .filter((q) => (args.role ? q.eq(q.field("role"), args.role) : q.truthy()))
      .order("desc")
      .take(100);
 
    return users;
  },
});
 
export const getPosts = query({
  args: {
    authorId: v.optional(v.id("users")),
    published: v.optional(v.boolean()),
  },
  handler: async (ctx, args) => {
    let query = ctx.db.query("posts");
 
    if (args.authorId) {
      query = query.withIndex("by_author", (q) =>
        q.eq("authorId", args.authorId)
      );
    }
 
    if (args.published !== undefined) {
      query = query.withIndex("by_published", (q) =>
        q.eq("published", args.published)
      );
    }
 
    const posts = await query.order("desc").take(100);
    return posts;
  },
});

Mutations

mutations.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
 
export const createUser = mutation({
  args: {
    name: v.string(),
    email: v.string(),
    role: v.optional(v.union(v.literal("USER"), v.literal("ADMIN"))),
  },
  handler: async (ctx, args) => {
    const userId = await ctx.db.insert("users", {
      name: args.name,
      email: args.email,
      role: args.role || "USER",
      createdAt: Date.now(),
    });
 
    return userId;
  },
});
 
export const createPost = mutation({
  args: {
    title: v.string(),
    content: v.string(),
    authorId: v.id("users"),
    tags: v.optional(v.array(v.string())),
  },
  handler: async (ctx, args) => {
    const postId = await ctx.db.insert("posts", {
      title: args.title,
      content: args.content,
      authorId: args.authorId,
      tags: args.tags || [],
      published: false,
      createdAt: Date.now(),
    });
 
    return postId;
  },
});

Next.js Integration

Server Component

app/users/page.tsx
import { getUsers } from "@/convex/queries";
 
export default async function UsersPage() {
  const users = await getUsers({ role: "USER" });
 
  return (
    <div className="container py-8">
      <h1 className="text-2xl font-bold mb-4">Users</h1>
      <div className="grid gap-4">
        {users.map((user) => (
          <div key={user._id} className="p-4 border rounded">
            <h2 className="font-semibold">{user.name}</h2>
            <p className="text-gray-600">{user.email}</p>
            <span className="text-sm text-gray-500">{user.role}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

Client Component with Real-time Updates

app/components/PostsList.tsx
"use client";
 
import { useQuery, useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
import { useState } from "react";
 
export function PostsList() {
  const posts = useQuery(api.posts.getPosts, { published: true });
  const createPost = useMutation(api.posts.createPost);
  const [newPost, setNewPost] = useState({ title: "", content: "" });
 
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await createPost({
      title: newPost.title,
      content: newPost.content,
      authorId: "YOUR_USER_ID", // Replace with actual user ID
    });
    setNewPost({ title: "", content: "" });
  };
 
  if (!posts) return <div>Loading...</div>;
 
  return (
    <div className="space-y-4">
      <form onSubmit={handleSubmit} className="space-y-4">
        <input
          type="text"
          value={newPost.title}
          onChange={(e) => setNewPost({ ...newPost, title: e.target.value })}
          placeholder="Post title"
          className="w-full p-2 border rounded"
        />
        <textarea
          value={newPost.content}
          onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
          placeholder="Post content"
          className="w-full p-2 border rounded"
        />
        <button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded">
          Create Post
        </button>
      </form>
 
      <div className="grid gap-4">
        {posts.map((post) => (
          <div key={post._id} className="p-4 border rounded">
            <h2 className="font-semibold">{post.title}</h2>
            <p className="text-gray-600">{post.content}</p>
            <div className="flex gap-2 mt-2">
              {post.tags.map((tag) => (
                <span
                  key={tag}
                  className="px-2 py-1 text-sm bg-gray-100 rounded"
                >
                  {tag}
                </span>
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Configuration

convex/config.ts
import { defineConfig } from "convex/schema";
 
export default defineConfig({
  schema: "./schema.ts",
  auth: {
    providers: ["clerk"], // or other auth providers
  },
});

Imports

imports.ts
import { defineSchema, defineTable } from "convex/schema";
import { v } from "convex/values";
import { Doc, Id } from "./_generated/dataModel";
import { query, mutation } from "./_generated/server";
import { useQuery, useMutation } from "convex/react";

Features:

  • Type-safe database schema
  • Real-time updates with Convex
  • Server and client components integration
  • Optimistic updates
  • Indexed queries
  • Proper error handling
  • Authentication integration
  • Clean and maintainable code structure
  • Modern React patterns
  • Efficient data fetching