Back to Projects

ChatterHub

Next.jsTypeScriptReactSocket.ioLiveKitPrismaMySQLClerkUploadThingTailwind CSSZustandTanStack QueryRadix UIZod
IMAGE — Server home page with channel list sidebar and member list
IMAGE — Text channel with messages, emoji picker, and file attachments
IMAGE — Video call in a video channel (LiveKit conference UI)
IMAGE — Audio channel with voice call participants
IMAGE — Direct message 1:1 conversation between two members
IMAGE — 1:1 video call between two members
IMAGE — Server creation modal with name and image upload
IMAGE — Server settings/edit modal
IMAGE — Invite link modal with copy button and regenerate option
IMAGE — Member management modal showing role dropdown (Guest/Moderator) and kick option
IMAGE — Channel creation modal (Text/Audio/Video type selector)
IMAGE — Message edit inline UI with save/cancel
IMAGE — Message delete confirmation modal
IMAGE — Mobile responsive view of server and channel navigation
IMAGE — Dark mode vs light mode comparison (side-by-side or just dark)
VIDEO
VIDEO
VIDEO
VIDEO
VIDEO
VIDEO
VIDEO
VIDEO
VIDEO
Server Home & Channels1 / 24

Overview

A real-time Discord-style communication platform built with Next.js 14, Socket.io, and LiveKit. Features server-based communities with text, audio, and video channels, 1:1 direct messaging, member management with role-based permissions, and infinite message pagination — all with real-time updates via WebSocket.

Key Features

Real-time & Messaging
  • Real-time messaging using Socket.io with WebSocket connections for instant message delivery across all connected clients
  • Send attachments as messages — images render as previews, PDFs show as downloadable links via UploadThing
  • Delete and edit messages in real time for all users — soft delete preserves message structure, edits show '(edited)' badge
  • 1:1 conversation between members with dedicated direct message threads and unique conversation constraints
  • 1:1 video calls between members using LiveKit with camera, microphone, and screen sharing support
  • Infinite loading for messages in batches of 10 using TanStack React Query with cursor-based pagination
  • WebSocket fallback: polling with alerts — React Query auto-polls every 1s when Socket.io connection drops
Servers & Channels
  • Create Text, Audio, and Video call channels — each type renders a different UI (chat interface or LiveKit media room)
  • Member management with role-based permissions — Admin can kick members, change roles between Guest and Moderator
  • Unique invite link generation and full working invite system — regenerate codes anytime, auto-join on visit
  • Server creation and customization with name, image upload, and invite code management
  • Authentication with Clerk — automatic profile creation on first sign-in, session-based route protection
Auth & Infrastructure
  • Beautiful UI using Tailwind CSS and Shadcn/UI with Radix UI primitives for accessible components
  • Full responsivity and mobile UI with collapsible sidebar navigation
  • Light / Dark mode toggle with next-themes
  • ORM using Prisma with MySQL database and relation mode for PlanetScale compatibility

Architecture Overview

How the system is structured from frontend to external services.

01

Frontend

Next.js 14 (App Router + Pages Router)React 18Tailwind CSSRadix UI / ShadcnZustand

App Router for page routes and REST APIs. Pages Router used for Socket.io server endpoints (required for custom HTTP server access). Zustand manages modal state globally. TanStack React Query handles infinite scroll pagination with automatic cache updates on real-time events.

02

Real-time Layer

Socket.io (WebSocket)TanStack React Query (fallback polling)

Socket.io server initialized on the Pages Router HTTP server, emitting events per channel/conversation. Client hooks listen to socket events and directly update the React Query cache for instant UI updates. If the WebSocket connection drops, React Query falls back to 1-second polling to keep data fresh.

03

Database

MySQL (PlanetScale)Prisma ORM (relationMode: prisma)

Seven models covering profiles, servers, members (with role enum), channels (with type enum), messages, conversations, and direct messages. Soft deletes for messages preserve chat history. Cursor-based pagination on message queries for efficient infinite scroll.

04

External Services

ClerkLiveKitUploadThing

Clerk handles authentication with automatic profile creation on first login. LiveKit provides WebRTC-based video/audio conferencing with JWT token generation per room. UploadThing manages image and PDF uploads for server avatars and message attachments.

Data Model

Core entities and how they relate to each other.

Profile

Has many Servers (as creator), Members (across servers), and Channels (as creator)

Maps Clerk userId to app data — created automatically on first sign-in with name, email, and avatar from Clerk

userId (unique, from Clerk)nameemailimageUrl

Server

Belongs to Profile (creator). Has many Members and Channels

A community space containing channels and members — created with a unique invite code for joining

nameimageUrlinviteCode (unique, UUID)profileId (creator)

Member

Belongs to Profile and Server. Has many Messages and DirectMessages. Has many Conversations (as memberOne or memberTwo)

Junction between Profile and Server — stores the user's role (ADMIN, MODERATOR, GUEST) within each server

role (enum: ADMIN | MODERATOR | GUEST, default: GUEST)profileIdserverId

Channel

Belongs to Profile (creator) and Server. Has many Messages

Communication channel within a server — type determines if it renders text chat, audio call, or video call UI

nametype (enum: TEXT | AUDIO | VIDEO, default: TEXT)profileId (creator)serverId

Message

Belongs to Member and Channel. updatedAt != createdAt indicates edited message

A message in a text channel — supports text content and optional file attachment (image/PDF). Uses soft delete to preserve chat structure

contentfileUrl (optional)deleted (boolean, soft delete)memberIdchannelId

Conversation

Belongs to two Members (memberOne and memberTwo). Has many DirectMessages

Unique 1:1 direct message thread between two members — bidirectional lookup ensures one conversation per pair

memberOneIdmemberTwoId@@unique([memberOneId, memberTwoId])

DirectMessage

Belongs to Member and Conversation

A message within a 1:1 conversation — same structure as channel Message with soft delete and file support

contentfileUrl (optional)deleted (boolean, soft delete)memberIdconversationId

Key Flows

Step-by-step walkthrough of the main user and system flows.

Real-time Message Flow (Socket.io)

  1. 1User types a message in the chat input and submits the form
  2. 2Client sends POST to /api/socket/messages with content, channelId, and serverId
  3. 3Server validates authentication via Clerk and checks user is a member of the server
  4. 4Message is saved to the database with memberId and channelId
  5. 5Socket.io emits event 'chat:{channelId}:messages' to all connected clients
  6. 6Client-side useChatSocket hook receives the event and directly updates the React Query cache
  7. 7UI re-renders instantly with the new message — useChatScroll auto-scrolls to bottom if user is near the bottom

Message Edit / Delete Propagation

  1. 1User clicks edit or delete on their own message (admins/moderators can delete any message)
  2. 2Client sends PATCH (edit) or DELETE (soft delete) to /api/socket/messages/[messageId]
  3. 3Server validates permissions: only owner can edit, owner + admin + moderator can delete
  4. 4For edit: message content is updated in the database
  5. 5For delete: message is soft-deleted — content set to 'This message has been deleted.', fileUrl cleared, deleted flag set to true
  6. 6Socket.io emits 'chat:{channelId}:messages:update' to all connected clients
  7. 7Client-side hook finds the message by ID in the React Query cache and replaces it — edit shows '(edited)' badge, delete shows italicized placeholder

Server Invite System

  1. 1Server admin opens the invite modal and copies the unique invite link (contains UUID invite code)
  2. 2Admin can regenerate the invite code anytime (PATCH /api/servers/[serverId]/invite-code generates a new UUID)
  3. 3Invited user visits /invite/[inviteCode] — Clerk authentication is required
  4. 4Server checks if user is already a member of this server
  5. 5If already a member: redirect to /servers/[serverId] (no duplicate join)
  6. 6If new: create a new Member record with GUEST role and redirect to the server

Video / Audio Call (LiveKit)

  1. 1User clicks on an Audio or Video channel (or starts a 1:1 video call)
  2. 2Client fetches a LiveKit access token from /api/livekit with the room ID (channelId or conversationId) and username
  3. 3Server generates a JWT token using LiveKit SDK with permissions: roomJoin, canPublish, canSubscribe
  4. 4Client connects to the LiveKit WebRTC server using the token and room URL
  5. 5LiveKit handles all media streaming — adaptive bitrate, peer connections, and UI controls via @livekit/components-react
  6. 6When user leaves the channel or navigates away, the LiveKit connection is automatically closed

Challenges I Solved

01

Hybrid App Router + Pages Router for Socket.io

The Problem

Next.js App Router doesn't expose the underlying HTTP server, which Socket.io needs to attach to. Pure App Router API routes can't initialize a WebSocket server.

How I Solved It

Used a hybrid approach: App Router for all page routes and REST APIs, Pages Router (/pages/api/socket/) for Socket.io endpoints. The Socket.io server is initialized once on the HTTP server object (res.socket.server.io) and reused across requests.

02

Real-time Cache Synchronization

The Problem

React Query manages paginated message data, but Socket.io events arrive outside of React Query's fetch cycle. Naively refetching would lose scroll position and create flickering.

How I Solved It

Socket events directly mutate the React Query cache via queryClient.setQueryData — new messages are prepended to the first page, edits/deletes find and replace the specific message across all pages. This gives instant updates without any refetch.

03

WebSocket Reliability & Fallback

The Problem

WebSocket connections can drop due to network issues, proxies, or firewalls. Users would see stale messages with no indication of the disconnect.

How I Solved It

The socket provider tracks connection state. When disconnected, React Query's refetchInterval kicks in at 1-second polling, and a UI indicator shows 'Polling every 1s' instead of 'Live: Real-time updates'. Messages stay fresh even without WebSocket.

04

Bidirectional Conversation Lookup

The Problem

When user A opens a DM with user B, we need to find the conversation regardless of who initiated it first. A simple where clause would miss conversations where the members are in the opposite order.

How I Solved It

The getOrCreateConversation utility checks both directions: first (memberOneId=A, memberTwoId=B), then (memberOneId=B, memberTwoId=A). A @@unique([memberOneId, memberTwoId]) constraint prevents duplicates.

Summary

ChatterHub demonstrates expertise in real-time web applications with Socket.io WebSocket integration, WebRTC video/audio conferencing via LiveKit, and complex state synchronization between server events and client-side React Query cache. The project showcases handling edge cases like WebSocket fallback, role-based message permissions, bidirectional conversation lookup, and hybrid Next.js router architecture.