Building a Slack Clone with Ruby on Rails
Jul 15, 2025 · 7 min read
I built a Slack clone from scratch with Rails. Not because the world needs another chat app — but because it's the perfect project to understand Rails' real-time capabilities, database design for concurrent messaging, and the tradeoffs of server-rendered UIs under load.
The Stack
- Rails 7 with Hotwire (Turbo + Stimulus)
- PostgreSQL with
FOR UPDATErow-level locking - Action Cable for WebSocket delivery
- Redis as the pub/sub adapter
Schema Design
Three core tables:
The messages table is the hot path. Every SELECT here needs an index on (channel_id, created_at). Without it, channel scrolling degrades to sequential scans in days.
Real-Time Delivery: Action Cable
When a user sends a message, two things happen:
- The message is persisted to Postgres
- A broadcast job pushes it to all connected clients in that channel
Action Cable handles the WebSocket upgrade behind the scenes. No separate WebSocket server needed.
The Hard Part: Optimistic Sends
The naive approach — wait for server acknowledgment before showing the message — adds 200-500ms latency. The fix was optimistic rendering:
- Insert the message into the DOM immediately on submit
- Queue the broadcast asynchronously
- If the server returns an error, rollback with a flash
Turbo Streams made this relatively painless. Stimulus controllers manage the temporary state before the broadcast confirms.
What I Learned
- Rails can do real-time — Action Cable with Redis scales further than most projects need
- Row-level locking matters — concurrent message reads without
FOR UPDATEproduce duplicate broadcasts under load - Optimistic sends are worth the complexity — the perceived speed difference is dramatic