WebSocket-based chat with typing indicators, read receipts, and online presence, built with a Node.js/Socket.io backend and a Flutter mobile frontend.
A single chat server is easy. The interesting problem is: what happens when you run more than one server instance (which any real app needs, for reliability and scale)? A message sent by a user connected to Server A needs to reach a user connected to Server B. A plain WebSocket server can't do this on its own — the two instances don't know about each other.
Redis pub/sub solves this. Every server instance subscribes to the same Redis channels. When a message is sent, it's published to Redis, and every server instance (including ones the sender isn't connected to) receives it and forwards it to its own connected clients. This is what makes the system horizontally scalable instead of a single point of failure.
Flutter App A ──WebSocket──┐
├──> Server Instance 1 ──┐
Flutter App B ──WebSocket──┘ │
├──> Redis (pub/sub)
Flutter App C ──WebSocket──┐ │
├──> Server Instance 2 ──┘
Flutter App D ──WebSocket──┘
JSON file (message history)
Locally, running one server instance, Redis has no visible effect on behavior — it only matters (and becomes necessary) once you run multiple instances behind a load balancer. That's intentional: the architecture is built to scale even though the demo runs on one instance.
- Backend: Node.js, Express, Socket.io, Redis (via ioredis + @socket.io/redis-adapter)
- Auth: JWT + bcrypt password hashing
- Persistence: lowdb (JSON file) — deliberately avoids native-compiled packages to skip Windows build-toolchain issues; swap for Postgres/MongoDB in production
- Frontend: Flutter (Dart), socket_io_client package
Requires Node.js and Redis running locally (or a Redis Cloud free instance).
cd server
npm install
cp .env.example .env
# edit .env if needed — defaults work for local Redis on default port
npm startServer runs on http://localhost:4000. Health check: GET /health.
POST /auth/signup—{ "username": "...", "password": "..." }POST /auth/login— same body shape
room:join—{ roomId }message:send—{ roomId, text }message:read—{ roomId, messageId }typing:start/typing:stop—{ roomId }
Server emits: room:history, message:new, message:readReceipt,
typing:update, presence:update
Requires Flutter SDK + Android Studio with an emulator set up.
cd client_flutter
flutter pub get
flutter runImportant: lib/config.dart points to http://10.0.2.2:4000 — this is
the special Android emulator alias for your host machine's localhost. If
you're testing on a physical device instead of an emulator, replace this
with your PC's actual LAN IP address.
Two users chatting in real-time across separate devices:

- Move online-presence tracking into Redis too (currently in-memory per instance, which means presence data doesn't sync across instances — only message delivery does, via the pub/sub adapter)
- Swap lowdb for Postgres/MongoDB for real persistence guarantees
- Add rate limiting on message sends to prevent spam/abuse
- Add pagination for message history instead of a flat "last 50" slice
