I wanted to build a real chat server - not the tutorial kind with Socket.io doing all the heavy lifting. So I did it properly. Raw WebSockets, Redis pub/sub, Postgres for history, presence tracking, typing indicators. The whole thing.
Built with Node.js, Redis, PostgreSQL, and a stubborn refusal to use a framework.
- Real-time messaging - raw WebSocket connections, no polling, no fallbacks
- User presence - see who's online, updates instantly when people join or leave
- Typing indicators - auto-expire after 4 seconds so they never get stuck
- Message history - last 50 messages loaded on join, persisted in Postgres forever
- Multi-room support - join any room by name, rooms are created on the fly
- Scales horizontally - Redis pub/sub means multiple server nodes work out of the box
- Invite anyone - throw it behind ngrok and share the URL
| Runtime | Node.js (pure http + ws) |
| Realtime fan-out | Redis Pub/Sub |
| Presence & typing | Redis hashes + sorted sets |
| Persistence | PostgreSQL |
| Client | Vanilla JS, single HTML file |
- Node.js 18+
- PostgreSQL
- Redis
- ngrok account (if you want to test it with the help of some friends)
git clone https://github.com/CosmoJelly/websocket-chat.git
cd websocket-chat
npm installcp .env.example .envPORT=3001
PG_HOST=localhost
PG_PORT=5432
PG_DB=wschat
PG_USER=postgres
PG_PASSWORD=postgres
REDIS_URL=redis://localhost:6379# Arch Linux
sudo pacman -S postgresql redis #(this could vary for your OS so use appropriately)
sudo -u postgres initdb --locale=en_US.UTF-8 -D /var/lib/postgres/data
sudo systemctl enable --now postgresql
sudo systemctl enable --now redis
sudo -u postgres createdb wschatnpm startOpen index.html in your browser and connect or use
xdg-open index.htmlExpose your local server with ngrok and send them the URL:
ngrok http 3001Tell them to open index.html and set the server field to:
wss://your-ngrok-url.ngrok-free.app/ws
Every message is JSON with a type field.
Client → Server
{ "type": "join", "roomId": "general", "username": "alice" }
{ "type": "message", "content": "hey" }
{ "type": "typing" }
{ "type": "stop_typing" }
{ "type": "heartbeat" }Server → Client
{ "type": "welcome", "userId": "...", "history": [...], "presence": {...} }
{ "type": "message", "username": "alice", "content": "hey", "createdAt": "..." }
{ "type": "presence", "username": "alice", "online": true }
{ "type": "typing", "username": "alice", "isTyping": true }websocket-chat/
├── server.js
├── chat.js
├── db.js
├── redis.js
├── index.html
├── docker-compose.yml
├── .env.example
└── package.json
rooms (id, name, created_at)
users (id, username, created_at, last_seen)
messages (id, room_id → rooms, user_id, username,
content, deleted_at, created_at)Do whatever you want with it.