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.
How the system is structured from frontend to external services.
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.
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.
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.
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.
Core entities and how they relate to each other.
Maps Clerk userId to app data — created automatically on first sign-in with name, email, and avatar from Clerk
userId (unique, from Clerk)nameemailimageUrlA community space containing channels and members — created with a unique invite code for joining
nameimageUrlinviteCode (unique, UUID)profileId (creator)Junction between Profile and Server — stores the user's role (ADMIN, MODERATOR, GUEST) within each server
role (enum: ADMIN | MODERATOR | GUEST, default: GUEST)profileIdserverIdCommunication 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)serverIdA 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)memberIdchannelIdUnique 1:1 direct message thread between two members — bidirectional lookup ensures one conversation per pair
memberOneIdmemberTwoId@@unique([memberOneId, memberTwoId])A message within a 1:1 conversation — same structure as channel Message with soft delete and file support
contentfileUrl (optional)deleted (boolean, soft delete)memberIdconversationIdStep-by-step walkthrough of the main user and system flows.
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.
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.
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.
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.
WebSocket connections can drop due to network issues, proxies, or firewalls. Users would see stale messages with no indication of the disconnect.
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.
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.
The getOrCreateConversation utility checks both directions: first (memberOneId=A, memberTwoId=B), then (memberOneId=B, memberTwoId=A). A @@unique([memberOneId, memberTwoId]) constraint prevents duplicates.
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.