Building a Slack Clone with Ruby on Rails

Jul 15, 2025 · 7 min read

Ruby on RailsWebSocketsAction CableReal-Time

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 UPDATE row-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:

  1. The message is persisted to Postgres
  2. 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:

  1. Insert the message into the DOM immediately on submit
  2. Queue the broadcast asynchronously
  3. 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 UPDATE produce duplicate broadcasts under load
  • Optimistic sends are worth the complexity — the perceived speed difference is dramatic