In modern web applications, real-time functionality and secure authentication are crucial features. In this post, I'll show you how to combine Convex DB's powerful real-time capabilities with Clerk's authentication system to build a responsive Next.js application.
What We'll Build
We'll create a simple collaborative task list where:
- Users can sign in with Clerk
- All connected users see updates in real-time
- Changes persist in Convex DB
- UI updates instantly without page refreshes
Prerequisites
- Node.js installed
- Basic knowledge of Next.js
- Convex and Clerk accounts (both have generous free tiers)
**Setting Up the Project
**First, create a new Next.js app:
npx create-next-app@latest convex-clerk-demo cd convex-clerk-demo
Installing Dependencies
npm install convex @clerk/nextjs @clerk/themes npm install convex-dev @clerk/types --save-dev
Configuring Clerk
Go to Clerk Dashboard and create a new application
Copy your publishable key and add it to your .env.local file:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key CLERK_SECRET_KEY=your_secret_key NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
Update your next.config.js:
/** @type {import('next').NextConfig} */ const nextConfig = { experimental: { serverActions: true, }, }; module.exports = nextConfig;
Create a middleware.js file at your project root:
import { authMiddleware } from "@clerk/nextjs"; export default authMiddleware({ publicRoutes: ["/", "/sign-in(.*)", "/sign-up(.*)"], }); export const config = { matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"], };
Wrap your app with the ClerkProvider in app/layout.tsx:
import { ClerkProvider } from '@clerk/nextjs' import { dark } from '@clerk/themes' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <ClerkProvider appearance={{ baseTheme: dark }}> <html lang="en"> <body>{children}</body> </html> </ClerkProvider> ) }
Setting Up Convex
- Install the Convex CLI globally:
npm install -g convex-dev
- Initialize Convex in your project:
npx convex init
Follow the prompts to create a new project or link to an existing one
Add your Convex deployment URL to .env.local:
NEXT_PUBLIC_CONVEX_URL="https://your-app-name.convex.cloud"
- Start the Convex dev server:
npx convex dev
Defining Our Data Schema
Create convex/schema.ts:
import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; export default defineSchema({ tasks: defineTable({ text: v.string(), completed: v.boolean(), userId: v.string(), }).index("by_user", ["userId"]), });
Creating Convex Mutations and Queries
- Create convex/tasks.ts for our task operations:
import { mutation, query } from "./_generated/server"; import { v } from "convex/values"; export const getTasks = query({ args: {}, handler: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) return []; const userId = identity.subject; return await ctx.db .query("tasks") .withIndex("by_user", q => q.eq("userId", userId)) .collect(); }, }); export const addTask = mutation({ args: { text: v.string() }, handler: async (ctx, { text }) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); const userId = identity.subject; await ctx.db.insert("tasks", { text, completed: false, userId }); }, }); export const toggleTask = mutation({ args: { id: v.id("tasks") }, handler: async (ctx, { id }) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); const task = await ctx.db.get(id); if (!task) throw new Error("Task not found"); if (task.userId !== identity.subject) { throw new Error("Not authorized"); } await ctx.db.patch(id, { completed: !task.completed }); }, });
Building the UI
Create app/page.tsx:
"use client"; import { useQuery, useMutation, useConvexAuth } from "convex/react"; import { api } from "@/convex/_generated/api"; import { SignInButton, UserButton, useUser } from "@clerk/nextjs"; import { useState } from "react"; export default function Home() { const { isAuthenticated } = useUser(); const { isLoading } = useConvexAuth(); const tasks = useQuery(api.tasks.getTasks) || []; const addTaskMutation = useMutation(api.tasks.addTask); const toggleTaskMutation = useMutation(api.tasks.toggleTask); const [newTaskText, setNewTaskText] = useState(""); if (isLoading) return <div>Loading...</div>; const handleAddTask = async (e: React.FormEvent) => { e.preventDefault(); if (!newTaskText.trim()) return; try { await addTaskMutation({ text: newTaskText }); setNewTaskText(""); } catch (err) { console.error("Error adding task:", err); } }; const handleToggleTask = async (id: string) => { try { await toggleTaskMutation({ id }); } catch (err) { console.error("Error toggling task:", err); } }; return ( <main className="max-w-2xl mx-auto p-4"> <div className="flex justify-between items-center mb-8"> <h1 className="text-2xl font-bold">Collaborative Task List</h1> {isAuthenticated ? <UserButton afterSignOutUrl="/" /> : <SignInButton />} </div> {isAuthenticated ? ( <> <form onSubmit={handleAddTask} className="mb-6 flex gap-2"> <input type="text" value={newTaskText} onChange={(e) => setNewTaskText(e.target.value)} placeholder="Enter a new task" className="flex-1 p-2 border rounded" /> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" > Add Task </button> </form> <ul className="space-y-2"> {tasks.map((task) => ( <li key={task._id} className="flex items-center gap-4 p-2 border rounded" > <input type="checkbox" checked={task.completed} onChange={() => handleToggleTask(task._id)} className="h-5 w-5" /> <span className={task.completed ? "line-through text-gray-500" : ""}> {task.text} </span> </li> ))} </ul> </> ) : ( <div className="text-center py-8"> <p className="mb-4">Sign in to view and manage your tasks</p> <SignInButton mode="modal"> <button className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"> Sign In </button> </SignInButton> </div> )} </main> ); }
Enabling Real-Time Updates
The magic of Convex is that our UI already updates in real-time! The useQuery hook automatically subscribes to changes in the underlying data. When any client makes a change through a mutation, all subscribed clients will receive the update instantly.
Deploying Your Application
- Deploy your Convex backend:
npx convex deploy
- Deploy your Next.js application to Vercel or your preferred hosting provider.
Security Considerations
Notice how we:
- Validate authentication in all Convex functions
- Check ownership before modifying tasks
- Only return tasks belonging to the current user
This ensures data privacy and security
Advanced Features to Explore
- Presence: Show which users are currently online
- Optimistic Updates: Improve perceived performance
- Realtime Notifications: Alert users of changes
- Collaborative Editing: Multiple users editing the same item
References
Convex Documentation
Clerk Documentation
Next.js Documentation
Conclusion
By combining Convex's real-time database with Clerk's authentication, we've built a secure, collaborative application with minimal boilerplate. The integration is seamless and provides a great developer experience while offering powerful features to end users.
Give it a try and let me know in the comments what you're building with this stack!
Top comments (0)