The moment you need realtime in a Next.js app — other users see changes instantly, live feeds, collaborative editing — you face a wall of setup: websockets, pub/sub, cache invalidation, optimistic updates. Convex collapses all of that into one useQuery call that stays current automatically.
This guide covers the full Convex + Next.js App Router setup from schema to production.
Full guide at stacknotice.com/blog/convex-nextjs-complete-guide-2026
Convex vs Drizzle+Neon vs Supabase
| Convex | Drizzle + Neon | Supabase | |
|---|---|---|---|
| Database | Document (TypeScript) | SQL (Postgres) | SQL (Postgres) |
| Realtime | Built-in, reactive | Manual | Subscriptions |
| Schema | TypeScript | TypeScript | SQL migrations |
| Self-hostable | No | Yes | Yes |
| Best for | Realtime, collaborative | Traditional CRUD | Firebase replacement |
Choose Convex when your app needs live collaboration, activity feeds, or any data multiple users see simultaneously.
Choose Drizzle + Neon when you need SQL, complex joins, or cost optimization at scale.
Schema in TypeScript
// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server'
import { v } from 'convex/values'
export default defineSchema({
tasks: defineTable({
title: v.string(),
done: v.boolean(),
userId: v.string(),
priority: v.union(v.literal('low'), v.literal('medium'), v.literal('high')),
createdAt: v.number(),
})
.index('by_user', ['userId'])
.index('by_user_done', ['userId', 'done']),
})
No SQL migration files. Convex generates TypeScript types from this automatically — your editor knows the shape of every document instantly.
Server Functions
// convex/tasks.ts
import { query, mutation } from './_generated/server'
import { v } from 'convex/values'
export const list = query({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) throw new Error('Unauthorized')
return await ctx.db
.query('tasks')
.withIndex('by_user', (q) => q.eq('userId', identity.subject))
.order('desc')
.collect()
},
})
export const create = mutation({
args: {
title: v.string(),
priority: v.union(v.literal('low'), v.literal('medium'), v.literal('high')),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) throw new Error('Unauthorized')
return await ctx.db.insert('tasks', {
title: args.title,
done: false,
userId: identity.subject,
priority: args.priority,
createdAt: Date.now(),
})
},
})
export const toggle = mutation({
args: { id: v.id('tasks'), done: v.boolean() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) throw new Error('Unauthorized')
const task = await ctx.db.get(args.id)
// Always verify ownership before mutating
if (!task || task.userId !== identity.subject) {
throw new Error('Not found or unauthorized')
}
await ctx.db.patch(args.id, { done: args.done })
},
})
No await db.connect(), no connection pool, no SQL strings. Functions auto-deploy when you save.
Reactive Client Queries
'use client'
import { useQuery, useMutation } from 'convex/react'
import { api } from '@/convex/_generated/api'
export function TaskList() {
const tasks = useQuery(api.tasks.list)
const toggle = useMutation(api.tasks.toggle)
if (tasks === undefined) return <div>Loading...</div>
return (
<ul>
{tasks.map((task) => (
<li key={task._id}>
<input
type="checkbox"
checked={task.done}
onChange={(e) => toggle({ id: task._id, done: e.target.checked })}
/>
<span className={task.done ? 'line-through' : ''}>{task.title}</span>
</li>
))}
</ul>
)
}
When any other user calls toggle, every component using useQuery(api.tasks.list) re-renders instantly. No websocket code, no subscription management, no cache invalidation. tasks === undefined means loading (not null — Convex's convention).
Auth with Clerk
// app/providers.tsx
'use client'
import { ClerkProvider, useAuth } from '@clerk/nextjs'
import { ConvexProviderWithClerk } from 'convex/react-clerk'
import { ConvexReactClient } from 'convex/react'
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
{children}
</ConvexProviderWithClerk>
</ClerkProvider>
)
}
ConvexProviderWithClerk handles token passing automatically. Your server functions call ctx.auth.getUserIdentity() to get the user — no manual JWT handling.
Server-Side Preloading (No Loading Spinner)
// app/dashboard/page.tsx
import { preloadQuery } from 'convex/nextjs'
import { api } from '@/convex/_generated/api'
import { auth } from '@clerk/nextjs/server'
export default async function DashboardPage() {
const { getToken } = auth()
const token = await getToken({ template: 'convex' })
const preloadedTasks = await preloadQuery(api.tasks.list, {}, {
token: token ?? undefined
})
return <TaskList preloadedTasks={preloadedTasks} />
}
Data arrives with the server-rendered HTML. The client component hydrates from it and stays reactive — no extra fetch, no loading state.
Scheduled Jobs (Built-in Cron)
// convex/crons.ts
import { cronJobs } from 'convex/server'
import { api } from './_generated/api'
const crons = cronJobs()
crons.daily(
'send due date reminders',
{ hourUTC: 8, minuteUTC: 0 },
api.notifications.sendDueDateReminders
)
export default crons
No external cron service, no separate worker. Scheduled functions are TypeScript alongside your other server functions.
Deploy
npx convex deploy
Atomic — either all functions deploy or none do. Set environment variables in Convex, not Vercel (except NEXT_PUBLIC_ vars):
npx convex env set RESEND_API_KEY re_...
npx convex env set CLERK_JWT_ISSUER_DOMAIN https://...
Convex changes the model: write a query, it stays current. The tradeoff is no SQL and no self-hosting. If either matters, Drizzle + Neon is the better call. For realtime, collaborative apps, or anything where "refresh to see new data" is unacceptable — Convex is the cleanest solution in the Next.js ecosystem right now.
Full guide with file storage, actions, pagination, and production checklist at stacknotice.com/blog/convex-nextjs-complete-guide-2026.
Top comments (0)