A production-quality real-time chat application built with Next.js, TypeScript, Convex, and Clerk.
| Layer | Technology |
|---|---|
| Frontend | Next.js 14 (App Router) + TypeScript |
| Styling | Tailwind CSS + shadcn/ui |
| Backend / DB | Convex (serverless, real-time) |
| Auth | Clerk |
| Deployment | Vercel |
git clone https://github.com/YOUR_USERNAME/tars-chat
cd tars-chat
npm install- Create a free account at clerk.com
- Create a new application
- In JWT Templates, create a new template named
convex - Copy your API keys to
.env.local
npx convex devThis will:
- Create a Convex project (or link an existing one)
- Generate the
convex/_generated/folder - Start watching for schema/function changes
In the Convex dashboard, go to Settings → Environment Variables and add:
CLERK_JWT_ISSUER_DOMAIN = https://your-app.clerk.accounts.dev
cp .env.local.example .env.local
# Fill in your Convex URL, Clerk keys, and Clerk JWT issuer domain# Terminal 1: Convex dev server
npx convex dev
# Terminal 2: Next.js dev server
npm run devVisit http://localhost:3000
users ← Synced from Clerk on login
conversations ← A chat channel (DM or group)
members ← Join table: user ↔ conversation + lastReadTime
messages ← Content, soft-deletable
presence ← Online/offline per user
typing ← Ephemeral typing indicators
Key design decisions:
- Join table for conversations: Allows group chat extension without schema changes
- Denormalized lastMessage on conversation: Avoids N+1 queries for sidebar preview
- Soft delete for messages: Preserves conversation history context
- Both isOnline + lastSeen for presence: Handles browser crashes gracefully
- Typed indexes on every foreign key: All queries use indexes, no full table scans
Convex's useQuery hook creates a reactive subscription. When data changes:
- Convex server detects the change
- Pushes the diff to all subscribed clients over WebSocket
- React re-renders automatically
No manual WebSocket setup, no polling.
app/layout.tsx (ClerkProvider + ConvexProvider)
app/chat/layout.tsx (user sync + presence setup)
Sidebar
UserSearch ← search + start DM
ConversationList ← all conversations with unread badges
app/chat/page.tsx ← "select a conversation" empty state
app/chat/[id]/page.tsx
ChatHeader ← name + online status + back button
MessageList ← smart scroll + date dividers
MessageItem ← bubble + delete + timestamp
TypingIndicator ← "Alex is typing..."
MessageInput ← send + typing notifications
- Authentication (Clerk: email + social login)
- User sync to Convex on login
- User list + real-time search
- One-on-one DMs (get-or-create pattern)
- Real-time messages via Convex subscriptions
- Sidebar with conversation previews
- Smart timestamp formatting (time / date+time / date+year)
- Date dividers in message history
- Empty states everywhere
- Responsive layout (mobile sidebar / desktop split)
- Online/offline presence with heartbeat
- Typing indicators with debounce + auto-clear
- Unread message count badges
- Marks-as-read on conversation open
- Smart auto-scroll with "↓ New messages" button
- Soft-delete messages ("This message was deleted")
- Skeleton loaders
-
npx convex deploy— deploys your Convex functions to production - In Convex dashboard, add
CLERK_JWT_ISSUER_DOMAINenv var for production - In Clerk dashboard, add your Vercel domain to Allowed Origins
- Import GitHub repo at vercel.com/new
- Add all environment variables from
.env.local:NEXT_PUBLIC_CONVEX_URL(use production URL fromnpx convex deployoutput)NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYCLERK_SECRET_KEYNEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-inNEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-upNEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/chatNEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/chat
- Deploy
- Test sign up with a new account
- Test DM flow end-to-end
- Test real-time with two browser tabs
- Test on mobile viewport
- Verify online/offline status updates