A modern, production-ready monorepo for secure, real-time messaging and WebRTC voice/video communication. Built with Next.js + React (frontend), Hono + Node.js (backend), PostgreSQL (persistence), Redis (real-time state), and WebSocket (live updates).
Status: Enterprise-grade foundation with reliability and scalability hardening in progress. Currently deployed and actively maintained.
- Real-time messaging with optimistic UI and persistent queue fallback
- Message operations: edit, delete, reactions, pinning
- Ephemeral messages with automatic expiration
- Typing indicators and presence awareness
- File uploads via Cloudinary integration
- Media support: images, audio messages, camera capture
- Link preview and smart message formatting
- Voice & video calls via WebRTC with ICE server support
- Audio settings: device selection, volume visualization, noise control
- Call signaling through WebSocket with reliable reconnection
- Screen sharing ready (WebRTC infrastructure in place)
- Anonymous authentication with room-scoped JWTs
- Room password protection with bcrypt hashing
- End-to-end encryption ready (encryption utilities included)
- CORS & security headers enforced
- Rate limiting on authentication and message endpoints
- XSS protection with DOMPurify sanitization
- Offline support with IndexedDB message queue
- Automatic reconnection with exponential backoff
- Request correlation via requestId for deduplication
- Persistence across browser tabs using localStorage
- Visibility-aware reconnect (reconnects when tab becomes active)
- Heartbeat mechanism for presence detection
- Network status indicator in UI
- TypeScript everywhere for type safety
- Monorepo with pnpm workspaces for shared utilities
- Shared validation schemas (Zod) between frontend & backend
- Dark/light theme support
- Responsive design with Tailwind CSS
- Component library with Radix UI + custom Aceternity components
SecureChat-nextjs/
├── apps/
│ ├── frontend/ # Next.js React frontend
│ │ ├── app/ # Pages & routes
│ │ ├── components/ # React components (chat, call, UI)
│ │ ├── contexts/ # Global state (Auth, Room, Theme)
│ │ ├── hooks/ # Custom hooks (WebSocket, messages, etc.)
│ │ ├── lib/ # Utilities (WS client, encryption, queue)
│ │ ├── types/ # TypeScript domain types
│ │ └── public/ # Static assets
│ │
│ └── backend/ # Hono Node.js backend
│ ├── src/
│ │ ├── config/ # Environment & constants
│ │ ├── lib/ # JWT, bcrypt, DB, Redis clients
│ │ ├── middleware/ # CORS, auth, error handling, rate limit
│ │ ├── routes/ # REST API endpoints
│ │ ├── services/ # Business logic
│ │ ├── repositories/ # Data access layer
│ │ ├── types/ # Hono types
│ │ ├── ws/ # WebSocket gateway & handlers
│ │ ├── workers/ # Background jobs (cleanup)
│ │ ├── app.ts # Hono app assembly
│ │ └── index.ts # Server entry point
│ ├── migrations/ # SQL migration files
│ └── Dockerfile # Container image
│
└── packages/
└── shared/ # Shared TypeScript package
├── src/
│ ├── types/ # API, domain, WebSocket types
│ ├── schemas/ # Zod validation schemas
│ └── index.ts # Exports
└── package.json
- Node.js 18+ (tested with 20+)
- pnpm 10.28.0+ (monorepo package manager)
- PostgreSQL 14+ (data persistence)
- Redis 6+ (real-time state, presence, typing)
- Docker (optional, for backend deployment)
1. Clone & install dependencies
git clone https://github.com/Harsha-code-per/SecureChat-nextjs.git
cd SecureChat-nextjs
pnpm install2. Configure environment
Frontend — Create apps/frontend/.env.local:
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
NEXT_PUBLIC_WS_URL=ws://localhost:8000/ws
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your_cloudinary_name
NEXT_PUBLIC_CLOUDINARY_UNSIGNED_PRESET=your_preset
NEXT_PUBLIC_GIPHY_API_KEY=your_giphy_keyBackend — Create apps/backend/.env:
NODE_ENV=development
PORT=8000
CORS_ORIGIN=http://localhost:3000
DATABASE_URL=postgresql://user:password@localhost:5432/securechat
REDIS_URL=redis://localhost:6379
JWT_ANON_SECRET=your-secret-key-min-32-chars-long-12345
JWT_ROOM_SECRET=your-room-secret-min-32-chars-long-12345
JWT_ANON_EXPIRY_HOURS=24
JWT_ROOM_EXPIRY_HOURS=8
BCRYPT_ROUNDS=10
ROOM_PASSWORD_PEPPER=your-pepper-string-min-16-chars
INTERNAL_CRON_SECRET=your-internal-secret-min-32-chars
# WebRTC TURN servers (optional)
TURN_URLS=turn:your-turn-server
TURN_USERNAME=turnuser
TURN_CREDENTIAL=turncred3. Setup databases
# Create PostgreSQL database
createdb securechat
# Run migrations (if applicable)
pnpm --filter backend db:migrate4. Start Redis (if not already running)
redis-server
# or with Docker:
docker run -d -p 6379:6379 redis:latest5. Run frontend & backend
# Terminal 1: Frontend (http://localhost:3000)
pnpm dev:frontend
# Terminal 2: Backend (http://localhost:8000)
pnpm dev:backend
# Or both together (if your shell supports it):
pnpm devPOST /v1/auth/anonymous— Create anonymous user → returns access tokenPOST /v1/auth/refresh— Refresh access tokenPOST /v1/auth/refresh-room— Refresh room-scoped tokenPOST /v1/auth/logout— Logout user
POST /v1/rooms— Create new roomGET /v1/rooms/:id/bootstrap— Get room + member infoPOST /v1/rooms/:id/join— Join room with passwordGET /v1/rooms/:id/settings— Get room configurationPOST /v1/rooms/:id/claim-owner— Claim room ownershipDELETE /v1/rooms/:id— Delete room
GET /v1/rooms/:id/messages— Message history (paginated)POST /v1/rooms/:id/messages— Send message (REST fallback)PUT /v1/rooms/:id/messages/:msgId— Edit messageDELETE /v1/rooms/:id/messages/:msgId— Delete messagePOST /v1/rooms/:id/messages/:msgId/react— Add reactionPOST /v1/rooms/:id/messages/:msgId/pin— Pin messageGET /v1/rooms/:id/messages/pinned— List pinned messages
GET /v1/rooms/:id/members— List room membersPOST /v1/rooms/:id/members/:userId/role— Update member rolePOST /v1/rooms/:id/members/:userId/kick— Kick memberPOST /v1/rooms/:id/members/:userId/ban— Ban member
POST /v1/rooms/:roomId/leave— Leave room explicitly
GET /v1/webrtc/config— Get TURN server configuration
GET /health— Service health check
Client → Server:
// Subscribe to room
{ event: 'room:subscribe', payload: { roomId }, requestId? }
// Send message
{ event: 'message:send', payload: { text, attachments?, requestId }, requestId }
// Typing indicator
{ event: 'typing:start', payload: { roomId }, requestId? }
{ event: 'typing:stop', payload: { roomId }, requestId? }
// Presence heartbeat
{ event: 'presence:heartbeat', payload: { roomId }, requestId? }
// Call signaling
{ event: 'call:offer', payload: { to, sdp, roomId }, requestId }
{ event: 'call:answer', payload: { to, sdp, roomId }, requestId }
{ event: 'call:ice', payload: { to, candidate, roomId }, requestId }
{ event: 'call:end', payload: { to, roomId }, requestId }
// Keepalive
{ event: 'ping' }Server → Client:
// Message events
{ event: 'message:new', payload: { id, text, userId, createdAt, requestId } }
{ event: 'message:edited', payload: { id, text, editedAt } }
{ event: 'message:deleted', payload: { id, deletedAt } }
{ event: 'message:expired', payload: { id } }
// User activity
{ event: 'presence:updated', payload: { userId, status, updatedAt } }
{ event: 'typing:updated', payload: { userId, isTyping } }
{ event: 'user:joined', payload: { userId, userName } }
{ event: 'user:left', payload: { userId } }
// Call signaling
{ event: 'call:offer', payload: { from, sdp } }
{ event: 'call:answer', payload: { from, sdp } }
{ event: 'call:ice', payload: { from, candidate } }
{ event: 'call:ended', payload: { from } }
// Keepalive
{ event: 'pong' }{
id: string;
name: string;
description?: string;
owner: string;
passwordHash?: string;
isPublic: boolean;
members: User[];
createdAt: Date;
expiresAt?: Date; // Ephemeral rooms auto-delete
}{
id: string;
roomId: string;
userId: string;
text: string;
attachments?: Attachment[];
reactions?: Record<string, string[]>;
isPinned: boolean;
editedAt?: Date;
expiresAt?: Date;
createdAt: Date;
}{
id: string;
name: string;
role: 'member' | 'moderator' | 'owner';
status: 'online' | 'away' | 'offline';
lastSeen: Date;
}{
id: string;
roomId: string;
initiator: string;
recipient: string;
status: 'ringing' | 'accepted' | 'rejected' | 'ended';
startedAt: Date;
endedAt?: Date;
}- Next.js 16 — React metaframework with App Router
- React 19 — UI library
- TypeScript 5 — Type safety
- Tailwind CSS 4 — Styling & responsive design
- Radix UI — Headless components
- React Hook Form + Zod — Form validation
- Framer Motion — Animations
- WebRTC API — Peer-to-peer audio/video
- IndexedDB — Offline message queue
- Sonner — Toast notifications
- Hono 4 — Lightweight web framework (edge-ready)
- Node.js — JavaScript runtime
- TypeScript 5 — Type safety
- Zod — Schema validation
- PostgreSQL 14+ — Relational database
- ioredis — Redis client
- jsonwebtoken — JWT authentication
- bcryptjs — Password hashing
- nanoid — ID generation
- Docker — Container runtime
- GitHub Actions — CI/CD (if configured)
- pnpm — Monorepo package manager
- tsx — TypeScript execution
AuthProvider— Authentication state, tokens, user identityRoomProvider— Current room context, member list, room settingsThemeProvider— Dark/light mode toggle
useMessages— Message timeline, optimistic updates, pagination, queue reconciliationuseUsers— Room presence list, member updatesusePresence— Subscribe/heartbeat, room join/leave, online statususeWebRTC— Call state machine, peer connections, media streamsuseWsConnection— WebSocket status tracking (connected|connecting|reconnecting|offline)useTypingIndicators— Typing awareness with debouncing
- localStorage — Room preferences, user settings, auth tokens
- IndexedDB — Message queue for offline-first reliability (securechat-queue database)
Flow:
- User joins anonymously →
POST /v1/auth/anonymous→ ReceivesaccessToken(short-lived, 24hr) - User joins room →
POST /v1/rooms/:id/join→ ReceivesroomToken(room-scoped, 8hr) - Tokens refreshed automatically before expiry via refresh endpoints
- On page reload, tokens restored from localStorage
Verification:
- All room-scoped operations require valid
roomToken(JWT) - Membership validated on each request via JWT claims
- Role-based access control: owner > moderator > member
- Passwords hashed with bcrypt (10+ rounds configured)
- JWT secrets configured via environment variables (minimum 32 chars)
- HTTPS recommended in production (use WSS for WebSocket)
- CORS enforced to prevent cross-origin abuse
- Security headers (CSP, X-Frame-Options, X-Content-Type-Options, etc.)
- XSS protection via DOMPurify on client, sanitization on server
- Rate limiting on auth endpoints (Redis-backed, sliding window)
- E2E encryption utilities included in
lib/client/e2ee.ts - Ready for integration; currently opt-in
- Use cases: sensitive communications, compliance requirements
- Secret-header authentication for internal cleanup routes
- Recommendation: Add IP allowlist + rotation policy for production
- Code splitting via Next.js dynamic imports & App Router lazy loading
- Image optimization with Next.js Image component
- Virtual scrolling for large message lists (React Window library)
- Request deduplication via requestId correlation (prevents duplicate UI renders)
- IndexedDB caching for offline-first UX (no network required)
- Lazy evaluation of expensive computations (useCallback, useMemo)
- Memoization of context providers to prevent unnecessary re-renders
- Horizontal scaling — Stateless design, all state in Redis/PostgreSQL
- WebSocket multiplexing — Room-based fanout via Hub pattern (no duplication)
- Redis presence — O(1) member lookups per room
- Connection pooling — PostgreSQL + Redis connection reuse (ioredis pooling)
- Rate limiting — Redis-backed token bucket per user/IP
- Batch operations — Efficient message pagination (cursor-based)
- Background workers — Async cleanup jobs (ephemeral expiry, stale presence)
- Persistent message queue — IndexedDB ensures no message loss on disconnect
- Request correlation —
requestIdprevents duplicate message creation - Heartbeat monitoring — Stale presence detection with configurable TTLs (default 30s)
- Reconnect logic — Exponential backoff + visibility-aware triggering
- Graceful degradation — Falls back to REST if WebSocket unavailable
- Acknowledgment tracking — Server confirms message receipt via requestId
1. User composes message in MessageInput
2. Optimistic message displayed immediately (local ID)
3. Message persisted to IndexedDB queue
4. WebSocket sends: { event: 'message:send', requestId, payload }
5. Backend validates & saves to PostgreSQL
6. Backend broadcasts: { event: 'message:new', requestId }
7. Frontend matches requestId, removes from queue, marks as confirmed
8. Queue item cleaned up
9. Message removed from IndexedDB
1. User composes; optimistic message shown
2. Message queued to IndexedDB (WS unavailable)
3. Connection restored (online event, visibility wake, tab refocus)
4. Queue flushed in order (100ms spacing between sends)
5. Each message sent with requestId for deduplication
6. Server confirms; queue items marked as sent
7. UI updates with server timestamps
8. Queue cleaned up
- Automatic detection — online/offline events, visibility API, heartbeat timeouts
- Exponential backoff — Configurable retry limits to prevent thundering herd
- State reconciliation — requestId correlation prevents duplicates
- No message loss — IndexedDB persists until successful confirmation
# Type checking
pnpm typecheck
# Linting
pnpm lint
# Building
pnpm build:frontend
pnpm build:backend
# Development
pnpm dev:frontend
pnpm dev:backend
pnpm dev # Both- Unit tests: Vitest or Jest with testing-library
- Integration tests: Supertest for backend API
- E2E tests: Playwright for critical user flows
- Coverage target: 80%+ for critical paths
- Configuration scaffold provided; add test files as needed
Build image:
cd apps/backend
docker build -t securechat-api:latest .Run container:
docker run -d \
-p 8000:8000 \
-e DATABASE_URL="postgresql://..." \
-e REDIS_URL="redis://..." \
-e JWT_ANON_SECRET="..." \
-e JWT_ROOM_SECRET="..." \
-e ROOM_PASSWORD_PEPPER="..." \
-e INTERNAL_CRON_SECRET="..." \
securechat-api:latestAutomated deployment (example with Azure Container Apps):
./deploy-backend.shBuild & deploy to Vercel (recommended for Next.js):
pnpm build:frontend
# Deploy dist to Vercel, Netlify, AWS Amplify, etc.Environment variables required:
NEXT_PUBLIC_APP_URL— Public app URLNEXT_PUBLIC_API_BASE_URL— Backend API URLNEXT_PUBLIC_WS_URL— WebSocket URL (must match backend)- Cloudinary & GIPHY keys (optional)
PostgreSQL migration:
# Create database
createdb securechat
# Run schema
psql securechat < apps/backend/migrations/001_initial_schema.sqlRedis setup:
- Managed services: AWS ElastiCache, Azure Cache for Redis, Upstash
- Self-hosted: Docker, systemd service, cloud VM
- Minimum: Redis 6.0+, 2GB memory recommended for production
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_APP_URL |
✓ | App root URL (e.g., https://app.securechat.io) |
NEXT_PUBLIC_API_BASE_URL |
✓ | Backend API base (e.g., https://api.securechat.io) |
NEXT_PUBLIC_WS_URL |
✓ | WebSocket URL (e.g., wss://api.securechat.io/ws) |
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME |
✗ | Cloudinary cloud for media uploads |
NEXT_PUBLIC_CLOUDINARY_UNSIGNED_PRESET |
✗ | Unsigned upload preset |
NEXT_PUBLIC_GIPHY_API_KEY |
✗ | GIPHY API key for GIF search |
| Variable | Required | Default | Description |
|---|---|---|---|
NODE_ENV |
✗ | development |
Environment mode (development|production) |
PORT |
✗ | 8000 |
Server port |
CORS_ORIGIN |
✓ | — | Frontend URL(s) |
DATABASE_URL |
✓ | — | PostgreSQL connection string |
REDIS_URL |
✓ | — | Redis connection string |
JWT_ANON_SECRET |
✓ | — | Anonymous JWT secret (min 32 chars) |
JWT_ROOM_SECRET |
✓ | — | Room JWT secret (min 32 chars) |
JWT_ANON_EXPIRY_HOURS |
✗ | 24 |
Anonymous token TTL in hours |
JWT_ROOM_EXPIRY_HOURS |
✗ | 8 |
Room token TTL in hours |
BCRYPT_ROUNDS |
✗ | 10 |
Password hashing rounds (10-12 recommended) |
ROOM_PASSWORD_PEPPER |
✓ | — | Password pepper salt (min 16 chars) |
INTERNAL_CRON_SECRET |
✓ | — | Internal API secret (min 32 chars) |
TURN_URLS |
✗ | — | TURN server URLs (comma-separated) |
TURN_USERNAME |
✗ | — | TURN server username |
TURN_CREDENTIAL |
✗ | — | TURN server credential |
PRESENCE_STALE_MS |
✗ | 30000 |
Presence heartbeat timeout (ms) |
TYPING_STALE_MS |
✗ | 3000 |
Typing indicator timeout (ms) |
EPHEMERAL_SWEEP_INTERVAL_MS |
✗ | 60000 |
Message cleanup job interval (ms) |
DISABLE_RATE_LIMITS |
✗ | — | Set to true in tests to disable rate limiting |
- Server-side idempotency key enforcement for
message:sendto prevent duplicates - Consolidate dual queue behavior (IndexedDB as authoritative source)
- Add integration tests for offline → reconnect → sync flows
- Replace Redis KEYS patterns with indexed structures (for scale)
- Align backend
.env.examplewith actualenv.tsconfiguration - Add IP allowlist + secret rotation for internal cleanup endpoints
- Structured logging & observability (Datadog, OpenTelemetry)
- WS metrics dashboard (reconnect reasons, queue depth, latency)
- Screen sharing support (WebRTC infrastructure ready)
- Call history & recording (optional)
- Message search & full-text indexing
- End-to-end encryption (utilities ready, requires UI/schema updates)
- Fork the repository
- Create feature branch:
git checkout -b feature/my-feature - Make changes with TypeScript & tests
- Commit with message:
git commit -m "feat: add my feature" - Push:
git push origin feature/my-feature - Open Pull Request with clear description of changes
- TypeScript — Strict mode enforced, no
anytype - Linting — ESLint with Next.js config (frontend), standard config (backend)
- Formatting — Follow existing code style
- Testing — Add tests for new features; maintain >80% coverage for critical paths
- Documentation — Update README & JSDoc comments for public APIs
MIT License — See LICENSE file for details.
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Security: For security concerns, please report privately
┌─────────────────────────────────────────────────────────────┐
│ Browser (Frontend) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Next.js React App │ │
│ │ - Auth, Room, Theme Contexts │ │
│ │ - Message, Presence, WebRTC Hooks │ │
│ │ - IndexedDB Queue (offline-first persistence) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ REST (auth, history) │ WebSocket (live) │
└───────────┼───────────────────────────────────────────────────┘
│ │
┌───────▼───────────────────────▼──────────┐
│ Node.js Hono Backend (API Layer) │
│ ┌────────────────────────────────────┐ │
│ │ REST Routes │ │
│ │ - Auth, rooms, messages, members │ │
│ ├────────────────────────────────────┤ │
│ │ WebSocket Gateway & Handlers │ │
│ │ - Room fanout (Hub pattern) │ │
│ │ - Message, presence, typing, call │ │
│ ├────────────────────────────────────┤ │
│ │ Services & Repositories │ │
│ │ - Message, Room, Presence service │ │
│ │ - Typing, Call service │ │
│ │ - Background workers │ │
│ └────────────────────────────────────┘ │
└────────────┬──────────────────┬──────────┘
│ SQL │ Redis
┌────────────▼──────┐ ┌────────▼──────────┐
│ PostgreSQL DB │ │ Redis Cache │
│ - Rooms & rules │ │ - Presence (TTL) │
│ - Messages │ │ - Typing (TTL) │
│ - Members │ │ - Rate limits │
│ - Call history │ │ - Session state │
└───────────────────┘ └───────────────────┘
1. User types "Hello" in frontend
2. LocalID generated, optimistic message shown
3. Message → IndexedDB queue (persisted)
4. WS: { event: 'message:send', requestId: 'req-123', payload: {...} }
5. Backend validates (Zod schema)
6. Backend → PostgreSQL INSERT
7. Backend checks Redis presence
8. Backend → WS broadcast: { event: 'message:new', requestId: 'req-123' }
9. Frontend receives, matches requestId
10. Frontend removes from queue, updates message with server ID & timestamp
11. Other users in room receive same broadcast
12. All UIs render: "Hello" with avatar, timestamp, read status
| File | Purpose |
|---|---|
apps/frontend/lib/ws.ts |
WebSocket client manager & event handling |
apps/frontend/lib/client/messageQueue.ts |
IndexedDB queue for offline persistence |
apps/frontend/hooks/useMessages.ts |
Message state, optimistic updates, reconciliation |
apps/frontend/hooks/usePresence.ts |
Room presence tracking & heartbeat |
apps/frontend/hooks/useWsConnection.ts |
WebSocket status tracking |
apps/frontend/components/chat/ChatInterface.tsx |
Main chat orchestrator |
| File | Purpose |
|---|---|
apps/backend/src/app.ts |
Hono app assembly & route mounting |
apps/backend/src/ws/gateway.ts |
WebSocket upgrade & connection management |
apps/backend/src/ws/handlers/messages.ts |
Message event handling & broadcasting |
apps/backend/src/services/messageService.ts |
Message business logic |
apps/backend/src/repositories/messageRepo.ts |
PostgreSQL message queries |
apps/backend/src/middleware/rateLimit.ts |
Redis-backed rate limiting |
| File | Purpose |
|---|---|
packages/shared/src/types/ws.ts |
WebSocket envelope & event types |
packages/shared/src/schemas/message.ts |
Message validation schemas |
packages/shared/src/types/api.ts |
REST API request/response DTOs |
- Clone the repository
- Install pnpm & dependencies
- Create
.env.localand.envfiles - Start PostgreSQL & Redis
- Run migrations
- Start frontend & backend dev servers
- Open http://localhost:3000
- Create a room and send a message
- Test WebRTC call (audio/video)
- Test offline mode (network throttling in DevTools)
- Check WebSocket reconnection behavior
Last Updated: 2026-04-24
Maintained by: Harshavardhan K
Repository: SecureChat-nextjs
Built with modern web standards, open-source libraries, and best practices from the community. Special thanks to Next.js, Hono, Radix UI, and the WebRTC community.
Happy building! 🚀