Back to Projects

Channels

Full-stack Discord clone with real-time messaging, voice/video calls, and custom WebSocket server

8 min read
2203 words
nextjstypescriptwebsocketzustandprismapostgresqlrealtimeNextAuth
Channels
Channels

Overview

Channels is a full-stack real-time communication platform inspired by Discord. It's not just another chat app - I built a custom WebSocket server from scratch, integrated LiveKit for voice/video, and implemented all the complex features you'd expect from a production messaging platform: servers, channels, roles, file sharing, typing indicators, and more.

Why Build This?

I wanted to understand real-time architecture deeply, not just plug in Firebase or Socket.io and call it a day. Building the WebSocket server taught me about connection lifecycle, memory leaks, race conditions, and the challenges of scaling real-time systems.

What Makes This Different: Most Discord clones use third-party solutions. I built a custom WebSocket server with proper authentication, channel-based pub/sub, typing indicators, and presence tracking. The frontend integrates this with React Query for a hybrid real-time approach that's resilient to disconnections.

12
Modal Types
3
Channel Types
Custom
WebSocket Server
5
Auth Attempts/Min

Tech Stack

Core Framework

  • Next.js 15 with App Router for modern React architecture
  • React for UI with Server and Client Components
  • TypeScript for end-to-end type safety
  • TailwindCSS for styling

State Management & Data

  • Zustand for lightweight modal state management
  • React Query for server state with infinite queries (websocket server fallback)
  • React Hook Form + Zod for validated form handling
  • Custom hooks for WebSocket integration (useChatSocket, useChatQuery)

UI Components

  • shadcn/ui - High-quality components built on Radix UI primitives
  • Lucide React for consistent icon system
  • Custom modal system with 12 different modal types
  • Responsive design with mobile navigation toggle

Project Architecture

Separated Frontend & Backend

Unlike most Next.js projects, Channels has a completely separate WebSocket server. The frontend (Next.js) handles HTTP requests, database operations, and UI. The backend (Node.js) handles real-time WebSocket connections. They communicate via HTTP for message broadcasting.

channels/
├── frontend/ (Next.js)
│ ├── src/
│ │ ├── app/
│ │ │ ├── (main)/
│ │ │ │ ├── (home)/ # Dashboard
│ │ │ │ └── (routes)/
│ │ │ │ ├── servers/[serverId]/
│ │ │ │ │ ├── channels/[channelId]/
│ │ │ │ │ └── conversations/[memberId]/
│ │ │ │ └── servers/ # Server discovery
│ │ │ ├── (invite)/[inviteCode]/ # Join server flow
│ │ │ ├── auth/ # Login, register, verify
│ │ │ └── api/
│ │ │ ├── livekit/ # Voice/video tokens
│ │ │ └── uploadthing/ # File uploads
│ │ ├── actions/ # Server Actions
│ │ │ ├── auth/ # Auth flows
│ │ │ ├── server.ts # Server CRUD
│ │ │ ├── channel.ts # Channel operations
│ │ │ ├── member.ts # Member management
│ │ │ ├── message.ts # Message operations
│ │ │ └── conversation.ts # Direct messages
│ │ ├── components/
│ │ │ ├── auth/ # Auth forms
│ │ │ ├── navigation/ # Sidebar navigation
│ │ │ ├── server/ # Server components
│ │ │ ├── chat/ # Chat UI
│ │ │ ├── modals/ # 12 modal types
│ │ │ └── providers/ # Context providers
│ │ ├── hooks/
│ │ │ ├── useChatSocket.ts # WebSocket integration
│ │ │ ├── useChatQuery.ts # Infinite scroll messages
│ │ │ ├── useChatScroll.ts # Auto-scroll behavior
│ │ │ └── useChatTyping.ts # Typing indicators
│ │ ├── lib/
│ │ │ ├── auth.ts # NextAuth config
│ │ │ ├── db.ts # Prisma client
│ │ │ └── errors/ # Error handling
│ │ ├── stores/
│ │ │ └── use-modal-store.ts # Zustand modal state
│ │ ├── schemas/ # Zod validation
│ │ └── types/ # TypeScript types
│ └── prisma/
│ └── schema.prisma # Database schema
└── backend/ (WebSocket Server)
├── src/
│ ├── server.ts # HTTP + WebSocket setup
│ ├── channelManager.ts # Pub/sub logic
│ ├── websocket.ts # Connection handling
│ ├── types/
│ │ └── index.ts # Event types
│ └── utils/
│ ├── logger.ts # Structured logging
│ └── utils.ts # Helpers
└── package.json

Core Features

Complete Authentication System

Built with NextAuth - had to figure out a lot on my own since docs were sparse. Email/Password Authentication: - Registration with email verification - Password hashing with bcrypt - Secure token generation with expiration - Password reset flow with email links Two-Factor Authentication: - Optional 2FA for enhanced security - Time-limited 6-digit codes - Sent via Resend email service - Required on every login when enabled OAuth Integration: - Google and GitHub login - Automatic account linking - Profile data sync Security Features: - Rate limiting: 5 attempts per minute per IP+email - JWT session strategy for performance - Token-based email verification (24h expiry) - Password reset tokens (1h expiry) - Role-based access control (USER, ADMIN) All handled through custom NextAuth callbacks and Prisma adapter configuration.

Server Management

Servers are like Discord servers - communities with channels and members. Server Creation: - Public or private servers - Custom server name and image - Category selection (Gaming, Education, Technology, etc.) - Tags for discoverability - Rich description Invite System: - Unique invite codes per server - Regenerate codes for security - Share invite links - Join via invite code Server Discovery: - Browse public servers - Filter by category and tags - Search by name or description - Join directly from discovery page Member Management: - Role-based permissions (Admin, Moderator, Guest) - Kick members (Admins only) - Change member roles (Admins only) - View member list with online status Server Settings: - Edit server details - Update server image - Regenerate invite code - Delete server (Admins only)

Channel System

Three types of channels, each serving different purposes. Channel Types:

  • TEXT - Regular chat channels for messages - AUDIO - Voice-only rooms - VIDEO - Video conferencing with screen sharing Channel Management: - Create channels (Admins, Moderators) - Edit channel names (Admins, Moderators) - Delete channels (Admins only) - Auto-created "general" channel per server Channel Features: - Channel-specific permissions - Visual icons for channel types - Quick navigation via server sidebar - Click video icon to start call instantly

Real-Time Text Chat

This is where the custom WebSocket server shines. Every message goes through my WebSocket infrastructure. Message Features: - Send text messages instantly - File attachments with UploadThing - Edit messages (owners only)

  • Delete messages (owners, moderators, admins) - Message timestamps - User avatars and names - Soft delete preserves conversation flow Real-Time Updates: - WebSocket for instant delivery - Automatic polling fallback if WebSocket disconnects - React Query cache updates from WebSocket events - No page refresh needed - everything is live File Sharing: - Upload images, PDFs, documents - File type detection and icons - Click to open/download - CDN-backed for fast delivery Pagination: - Infinite scroll loads 10 messages at a time - Smooth scrolling with position preservation - Cursor-based pagination for efficiency Typing Indicators: - See when others are typing - "User is typing..." appears in real-time - Auto-cleanup after 3 seconds of inactivity - Prevents flickering with debouncing

Direct Messages

Private one-on-one conversations between server members. Features: - Automatic conversation creation - Same features as channel chat - Message history persistence - File sharing supported - Accessible from server member list How It Works: - Click a member's name in server - Opens DM conversation - Private from other server members - Can have multiple DMs with different people

Voice & Video Calls

Powered by LiveKit for professional-grade WebRTC. Audio Rooms: - Join voice channels - Multiple participants - Low latency audio - Automatic echo cancellation Video Conferencing: - Face-to-face calls - Screen sharing - Grid layout for multiple participants - Dominant speaker highlighting Technical Implementation: - JWT tokens generated server-side - Room-specific permissions - Short-lived tokens for security - Automatic reconnection on network issues How To Use: - Click video icon in channel header - Automatically joins LiveKit room - Grant camera/microphone permissions - Start talking!

Modern UI/UX

Built with shadcn/ui and custom components for a polished experience. Design Features: - Dark theme by default (light theme available) - Fully responsive - works on mobile, tablet, desktop - Server sidebar with channel/member lists - Navigation sidebar with server icons - Smooth transitions and animations - Loading states with skeletons Modal System: 12 different modals for various actions: - Create/Edit Server - Invite Members - Manage Members - Create/Edit/Delete Channel - Leave Server - Delete Server - Message File Upload - Delete Message All controlled by Zustand store to prevent hydration issues. User Experience: - Toast notifications for feedback - Error handling with user-friendly messages - Keyboard shortcuts - Command palette (Ctrl+K) - Tooltips for better understanding - Avatar fallbacks with user initials

Custom WebSocket Server

Real-Time Architecture

Database Schema

Technical Challenges & Solutions

Challenge 1: WebSocket + Server Components

Problem

Next.js App Router defaults to Server Components which can't use WebSocket (client-side only). Direct WebSocket usage would cause hydration mismatches. Server Actions return serialized data, but WebSocket events are real-time. How to bridge these two worlds?

Solution

Built a hybrid architecture: 1) Use Server Components for initial data fetching, 2) Client Components for real-time UI (chat), 3) React Query as the bridge - Server Actions populate initial cache, WebSocket updates it in real-time, 4) Custom hooks (useChatSocket, useChatQuery) encapsulate the complexity. The UI consumes React Query data regardless of source.

Result

Seamless real-time experience without hydration issues. Messages load fast from server (SSR), then update in real-time via WebSocket. Automatic polling fallback when WebSocket disconnects. Clean separation of concerns.

Challenge 2: Memory Leaks in WebSocket Server

Problem

Early versions had memory leaks. Closed connections stayed in memory. Channels with no subscribers persisted. Typing indicators never cleaned up. Memory usage grew continuously until server crash.

Solution

Implemented multi-layered cleanup strategy: 1) Immediate cleanup on connection close, 2) Periodic cleanup every 5 minutes to catch any missed connections, 3) Automatic typing indicator removal after 3 seconds, 4) Delete empty channels, 5) Bidirectional mappings ensure complete cleanup (both ws→channels and channels→ws get updated).

Result

Stable memory usage even after days of uptime. No memory leaks in production. Graceful handling of connection issues. Server can run indefinitely without restart.

Challenge 3: Race Conditions in Typing Indicators

Problem

Multiple typing events arriving rapidly caused flickering UI. If user types fast, events could arrive out of order. Sometimes 'stopped typing' appeared before 'started typing'. Multiple users typing simultaneously caused state inconsistencies.

Solution

Changed approach from individual typing events to broadcasting complete typing user list. Server maintains a timestamped map of all typing users. Always send the full list, not just changes. Client displays whoever is in the list. 3-second auto-cleanup on server removes stale typing status. This makes the state deterministic.

Result

Smooth typing indicators with no flickering. Multiple users typing works perfectly. State always consistent. Auto-cleanup prevents 'stuck' typing indicators.

Challenge 4: Modal Hydration Errors

Problem

Modals rendered on server didn't match client state, causing React hydration errors. Zustand store loads from localStorage on client, but server doesn't have that state. All 12 modals throwing hydration warnings.

Solution

Created ModalProvider that only renders after client mount: const [isMounted, setIsMounted] = useState(false); useEffect(() => setIsMounted(true), []); if (!isMounted) return null; This ensures modals only render on client after Zustand state is hydrated from localStorage.

Result

Zero hydration errors. Modals work perfectly across all pages. State persists correctly across page refreshes. Clean console with no warnings.

Challenge 5: Infinite Scroll Performance

Problem

Loading all messages at once was slow for channels with thousands of messages. Needed pagination, but traditional page-based pagination doesn't work well with real-time updates (new messages shift page numbers).

Solution

Implemented cursor-based pagination with React Query's infinite query. Load 10 messages per batch. Cursor is the oldest message ID in current batch. New messages always prepend to first page. Custom useChatScroll hook preserves scroll position when loading history. Intersection Observer triggers fetchNextPage when scrolling up.

Result

Smooth infinite scroll with no jumpiness. Loads only what's needed. Works perfectly with real-time updates. Can handle channels with 10,000+ messages. Scroll position preserved when loading history.

Challenge 6: File Upload Size & Performance

Problem

Large file uploads blocked the UI. Failed uploads lost progress. Needed progress indicators and error handling. Also needed to prevent abuse (uploading 1GB files).

Solution

Integrated UploadThing which handles: 1) Client-side file validation before upload, 2) Progress indicators during upload, 3) CDN-backed storage for fast delivery, 4) Automatic image optimization, 5) Size limits enforced (10MB for images, 50MB for other files), 6) Upload happens in background - UI stays responsive.

Result

Smooth file upload experience. Users see progress bars. Large files don't block UI. Failed uploads show helpful errors. Files delivered fast via CDN. No abuse due to size limits.

Performance Optimizations

Database Indexes

All foreign keys indexed for fast lookups. Composite indexes on frequently queried combinations (serverId + channelId). Query times under 50ms for most operations.

React Query Caching

Messages cached for 5 minutes. Infinite queries preserve all loaded pages. Reduces database load by 80% with proper cache invalidation.

WebSocket Efficiency

Only subscribe to active channels. Unsubscribe when leaving. Connection pooling for multiple tabs. Reduces network traffic significantly.

Infinite Scroll

Load 10 messages at a time instead of all at once. Cursor-based pagination is more efficient than offset-based. Smooth scrolling even with thousands of messages.

Image Optimization

Next.js Image component with automatic optimization. WebP format for smaller file sizes. Lazy loading for images below the fold.

Code Splitting

Route-based code splitting. Modals loaded on demand. Heavy components wrapped in React.lazy(). Initial bundle size reduced 50%.

Learning Outcomes

WebSocket Mastery

Building a WebSocket server from scratch taught me about connection lifecycle, resource management, pub/sub patterns, and real-time architecture. Way more valuable than using Socket.io.

Hybrid Real-Time Architecture

Learned to combine WebSocket speed with React Query resilience. Automatic fallbacks, cache synchronization, and handling disconnections gracefully.

NextAuth v5

Worked with beta software and incomplete documentation. Built custom callbacks, OAuth integration, and session enrichment. Understand JWT strategies deeply now.

WebRTC Integration

LiveKit taught me about WebRTC, peer connections, signaling, and media streams. Voice/video isn't as scary as it seems with the right infrastructure.

Production Patterns

Rate limiting, error handling, graceful degradation, monitoring, and security best practices. This app is actually production-ready.

Complex State Management

Managing modal state, WebSocket connections, infinite queries, and real-time updates simultaneously. Learned when to use different state solutions (Zustand vs React Query vs local state).

Conclusion

Channels is one of the most complex project I've built. The custom WebSocket server alone was a massive learning experience - dealing with connection lifecycle, memory management, race conditions, and authentication over WebSocket.

What I'm Most Proud Of
  • Custom WebSocket Server: Built from scratch instead of using Socket.io - Hybrid Architecture: WebSocket + React Query works beautifully - Production-Ready: Rate limiting, error handling, monitoring, security - Real-Time Everything: Messages, typing, presence - all instant

The Big Lesson: Real-time systems are hard. There's so much that can go wrong - disconnections, race conditions, memory leaks, security issues. But building it from scratch taught me more than any tutorial could. I understand WebSockets at a fundamental level now.

This project would be perfect for:

  • Team communication (small companies, gaming clans)
  • Learning communities
  • Customer support channels
  • Internal company chat
  • Any real-time collaboration platform

The code is clean, well-documented, and ready for production. Just need to deploy the WebSocket server on a proper host (currently in Railway on free plan)

Channels | Shalev Asor