A real-time chat application built for 50+ concurrent users
Features · Architecture · Getting Started · API Reference · WebSocket Events · Contributing
- ⚡ Real-time messaging via persistent WebSocket connections
- 🏠 Public & private rooms — create, join, leave, and invite users via shareable links
- 💬 Direct messages (DMs) — one-on-one conversations with contacts
- 👥 Contact system — send, accept, and reject contact requests
- ✍️ Typing indicators — live "user is typing…" notifications
- 🟢 Online presence — see who's currently active across all rooms
- 🧑💼 User profiles — display name and profile picture support
- 📜 Persistent message history — stored in PostgreSQL, loaded on room join
- 🔴 Unread counts — per-room badge counts for missed messages
- 🔐 JWT-based WebSocket auth — all connections authenticated via Appwrite-issued JWTs
┌─────────────────────────────────┐
│ React Client │
│ (Vite + Appwrite SDK) │
│ │
│ Auth ──────── Appwrite │
│ Realtime ───── WebSocket │
│ Data ──────── Express REST │
└────────────┬────────────────────┘
│ HTTP + WebSocket
┌────────────▼────────────────────┐
│ Express Server │
│ (Node.js + ws library) │
│ │
│ REST API ──── PostgreSQL │
│ WebSocket ──── in-memory Maps │
│ Auth verify ── Appwrite API │
└────────────┬────────────────────┘
│
┌────────┴────────┐
│ │
┌───▼────┐ ┌──────▼──────┐
│ PG DB │ │ Appwrite │
│ msgs │ │ auth/users │
│ rooms │ └─────────────┘
│contacts│
└────────┘
The server maintains two in-memory structures for real-time routing:
onlineUsers: Map<userId, Set<WebSocket>>— all active connections per userrooms: Map<roomId, Set<WebSocket>>— all subscribers per room
- Node.js >= 18.0.0
- PostgreSQL database — local, or hosted (Supabase, Neon, Railway)
- Appwrite project — cloud.appwrite.io or self-hosted, with Email/Password auth enabled
git clone https://github.com/your-username/realtime-chat-app.git
cd realtime-chat-app- Create a project at cloud.appwrite.io.
- Enable Email/Password authentication under Auth → Settings.
- Copy your Project ID and API Endpoint — you'll need them in both
.envfiles below.
cd server
cp .env.example .env # then fill in your values
npm installserver/.env reference
# Server
PORT=10000
# PostgreSQL
PGUSER=your_db_user
PGHOST=your_db_host
PGDATABASE=your_db_name
PGPASSWORD=your_db_password
PGPORT=5432
# Appwrite (used to verify JWTs)
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
APPWRITE_PROJECT_ID=your_appwrite_project_id
# CORS — comma-separated list of allowed frontend origins
CORS_ORIGIN=http://localhost:5173Run the following SQL against your PostgreSQL database before starting the server:
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT,
status TEXT DEFAULT 'offline',
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS rooms (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT DEFAULT 'public',
created_by TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS room_members (
room_id TEXT REFERENCES rooms(id),
user_id TEXT REFERENCES users(id),
last_read_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (room_id, user_id)
);
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
room_id TEXT REFERENCES rooms(id),
user_id TEXT,
user_email TEXT,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS contacts (
id SERIAL PRIMARY KEY,
user_id TEXT REFERENCES users(id),
contact_id TEXT REFERENCES users(id),
status TEXT DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (user_id, contact_id)
);cd ../client
cp .env.example .env # then fill in your values
npm installclient/.env reference
VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
VITE_APPWRITE_PROJECT_ID=your_appwrite_project_id
VITE_API_URL=http://localhost:10000
VITE_WS_URL=ws://localhost:10000Start the server (terminal 1):
cd server
npm run dev # nodemon — auto-reloads on save
# or
npm start # plain nodeStart the client (terminal 2):
cd client
npm run devOpen http://localhost:5173 in your browser. 🎉
| Layer | Recommended platforms |
|---|---|
| Server | Railway, Render, Fly.io |
| Client | Vercel, Netlify, Cloudflare Pages |
| Database | Supabase, Neon, Railway |
After deploying the server, update the client's
VITE_API_URLandVITE_WS_URLto your server's public URL, and add the client's origin to the server'sCORS_ORIGINenv var.
| Variable | Required | Default | Description |
|---|---|---|---|
PORT |
No | 10000 |
HTTP / WebSocket server port |
PGUSER |
Yes | — | PostgreSQL username |
PGHOST |
Yes | — | PostgreSQL host |
PGDATABASE |
Yes | — | PostgreSQL database name |
PGPASSWORD |
Yes | — | PostgreSQL password |
PGPORT |
No | 5432 |
PostgreSQL port |
APPWRITE_ENDPOINT |
Yes | http://localhost/v1 |
Appwrite API endpoint |
APPWRITE_PROJECT_ID |
Yes | — | Appwrite project ID |
CORS_ORIGIN |
No | — | Comma-separated list of allowed client origins |
| Variable | Required | Default | Description |
|---|---|---|---|
VITE_APPWRITE_ENDPOINT |
Yes | — | Appwrite API endpoint |
VITE_APPWRITE_PROJECT_ID |
Yes | — | Appwrite project ID |
VITE_API_URL |
No | http://localhost:3000 |
Express REST API base URL |
VITE_WS_URL |
No | ws://localhost:3000 |
WebSocket server URL |
All REST endpoints except /health require an x-user-id header with the authenticated user's Appwrite ID.
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Server health check (uptime, users, rooms) |
| GET | /api/rooms |
List rooms the current user belongs to |
| POST | /api/rooms/create |
Create a new room |
| POST | /api/rooms/join |
Join an existing room by ID |
| POST | /api/rooms/leave |
Leave a room |
| POST | /api/rooms/dm |
Create or retrieve a DM room with another user |
| POST | /api/rooms/invite |
Generate an invite link for a room |
| POST | /api/rooms/update |
Update a room's name |
| GET | /api/messages/:roomId |
Fetch paginated message history for a room |
| GET | /api/users/search |
Search users by email (?email=...) |
| GET | /api/contacts |
List accepted contacts |
| GET | /api/contacts/requests |
List pending incoming contact requests |
| POST | /api/contacts/request |
Send a contact request to a user |
| POST | /api/contacts/accept |
Accept a contact request |
| POST | /api/contacts/reject |
Reject a contact request |
After connecting, the client must authenticate by sending a JOIN message with a valid Appwrite JWT. All messages are JSON-encoded.
| Type | Payload fields | Description |
|---|---|---|
JOIN |
token, userId, email |
Authenticate and register connection |
JOIN_ROOM |
roomId |
Subscribe to a room's messages |
ROOM_MESSAGE |
roomId, content |
Send a message to a room |
TYPING_START |
roomId |
Notify room that user is typing |
TYPING_STOP |
roomId |
Notify room that user stopped typing |
PING |
— | Keep-alive ping |
| Type | Payload fields | Description |
|---|---|---|
PONG |
— | Keep-alive response |
JOINED_ROOM |
roomId |
Confirms room subscription |
ROOM_MESSAGE |
roomId, userId, content, createdAt, userEmail |
New message in a room |
USER_JOINED |
roomId, userId, email |
A user joined the room |
USER_LEFT |
roomId, userId |
A user left or disconnected |
USER_TYPING |
roomId, userId, isTyping |
Typing indicator update |
USER_STATUS |
userId, status |
User came online or went offline |
ERROR |
message |
Error response from the server |
realtime-chat-app/
├── client/ # React frontend (Vite)
│ ├── src/
│ │ ├── App.jsx # Main app — auth, chat, contacts, rooms
│ │ ├── main.jsx # React entry point
│ │ ├── index.css # Global styles
│ │ └── appwrite.js # Appwrite client setup
│ ├── index.html
│ ├── vite.config.js
│ ├── .env.example
│ └── package.json
│
└── server/ # Node.js backend
├── index.js # Express app, REST endpoints, WebSocket handler
├── db.js # PostgreSQL connection pool
├── .env.example
└── package.json
| Command | Description |
|---|---|
npm start |
Start the server with Node |
npm run dev |
Start with nodemon (auto-reload on save) |
| Command | Description |
|---|---|
npm run dev |
Start the Vite dev server |
npm run build |
Build for production → dist/ |
npm run preview |
Preview the production build locally |
Contributions are welcome! To get started:
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature-name - Commit your changes:
git commit -m 'Add some feature' - Push to your branch:
git push origin feature/your-feature-name - Open a Pull Request
For major changes, please open an issue first so we can discuss the approach.
This project is licensed under the MIT License — see the LICENSE file for details.
Made with ❤️ · Report a Bug · Request a Feature