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)