Real-time Chat Application using Rails Action Cable
A simple real-time chat application built with Rails 7.2 and Action Cable for WebSocket communication
README
Rails Chat App with Action Cable
A simple real-time chat application built with Rails 7.2 and Action Cable.
What you can build with this Blueprint
- Real-time chat application with WebSocket
- Multiple chat rooms
- User presence detection (who's online)
- Message history
- Simple user authentication
- Responsive design with Tailwind CSS
Target Stack
- Language: Ruby 3.2+
- Framework: Rails 7.2
- Real-time: Action Cable (WebSocket)
- CSS: Tailwind CSS
- DB: PostgreSQL (Docker container)
- Cache: Redis (for Action Cable adapter)
- Editor: VS Code
- AI Agent: Claude 4.5
Features
✅ User authentication (username/password)
✅ Create and join chat rooms
✅ Real-time messaging via WebSocket
✅ User presence (online/offline status)
✅ Message history
✅ Responsive mobile-friendly UI
✅ Emoji support in messages
How to use this Blueprint
For new Rails project
- Download and extract this Blueprint
- Place
example-chat-blueprint/in your desired project location - Open the directory in VS Code
- Let the AI agent read
example-chat-blueprint/README.mdfirst - Agent will create Rails app in the same parent directory
- Execute implementation tasks in
docs/06_tasks.mdsequentially
For existing Rails project
- Download and extract this Blueprint
- Place
example-chat-blueprint/in your Rails project root (same level asapp/,config/) - Optionally add to
.gitignoreif you don't want to commit design docs - Let the AI agent read
example-chat-blueprint/README.md - Agent will implement features into your existing Rails app
Directory structure example
my-chat-project/ # Project root
├── example-chat-blueprint/ # This Blueprint (design docs)
│ ├── README.md
│ ├── blueprint.yml
│ └── docs/
│ ├── 01_requirements.md
│ ├── 02_architecture.md
│ ├── 03_data_model.md
│ ├── 04_user_flows.md
│ ├── 05_agent_usage.md
│ ├── 06_tasks.md
│ └── 99_agent_notes.md
└── chat-app/ # Rails app (created by agent)
├── app/
├── config/
├── db/
└── ...
Quick Start (After Implementation)
Start Redis (required for Action Cable)
redis-server
Start Rails server
cd chat-app
bin/rails server
Visit the app
Open http://localhost:3000 in your browser
Development Flow
- Read
docs/05_agent_usage.mdto understand how to work with AI agents - Follow tasks in
docs/06_tasks.mdsequentially - Test each feature after implementation
- Refer to
docs/99_agent_notes.mdfor tips and gotchas
Testing Real-time Features
- Open multiple browser windows (or use incognito mode)
- Sign in as different users
- Join the same room and start chatting
- Messages should appear in real-time across all windows
- User presence should update when users join/leave
Common Issues
- WebSocket not connecting: Check Redis is running
- Messages not appearing: Check browser console for JS errors
-
Cable connection closed: Verify Action Cable is configured in
config/cable.yml
Next Steps (Post-MVP)
- Direct messages between users
- File/image sharing in chat
- Message reactions (like, emoji reactions)
- Typing indicators
- Read receipts
- Push notifications
- Voice/video chat integration
- Chat room moderation features
Learn More
Requirements
Requirements
Functional Requirements (MVP)
User Authentication
- Sign up with username and password
- Sign in to access chat rooms
- Sign out functionality
- Current user detection and display
- Simple password authentication (bcrypt)
Chat Rooms
- List all rooms (public rooms visible to all logged-in users)
- Create new room with name and description
- Join existing room by clicking on it
- Leave room to go back to room list
- Room information (name, description, participant count)
Real-time Messaging
- Send messages in a chat room
- Receive messages in real-time via WebSocket
- Display sender username with each message
- Timestamp for each message
- Message history (load previous messages when joining)
- Scroll to bottom automatically when new messages arrive
- Emoji support in messages
User Presence
- Online status - show who's currently in a room
- User list - display active participants
- Join/leave notifications - notify when users enter/exit
- Active user count in room list
UI/UX
- Responsive design - works on mobile and desktop
- Clean interface with Tailwind CSS
- Loading states for async operations
- Error messages for failed actions
- Empty states for no messages/rooms
- Message input with send button and Enter key support
Non-Functional Requirements
Performance
- Messages delivered within 100ms
- Handle 100+ concurrent users per room
- Efficient database queries (n+1 prevention)
- Redis caching for Action Cable
Security
- Password hashing with bcrypt
- CSRF protection (Rails default)
- WebSocket origin validation
- Sanitize user input (prevent XSS)
- Authenticated WebSocket connections
Reliability
- Graceful WebSocket reconnection
- Connection status indicator
- Error recovery for failed sends
- Data persistence (messages saved to DB)
Usability
- Intuitive navigation
- Clear visual hierarchy
- Accessible keyboard shortcuts
- Mobile-friendly touch targets
- Readable text contrast
Out of Scope (v1)
Features Not Included
- ❌ Direct messages (1-on-1 chat)
- ❌ File/image uploads
- ❌ Message editing/deletion
- ❌ Message reactions
- ❌ Typing indicators
- ❌ Read receipts
- ❌ User profiles/avatars
- ❌ Email verification
- ❌ Password reset
- ❌ OAuth login
- ❌ Private rooms (invitation-only)
- ❌ Room ownership/moderation
- ❌ Search messages
- ❌ Message threading
- ❌ Voice/video chat
- ❌ Push notifications
- ❌ Mobile app
User Stories
As a visitor
- I want to sign up with a username so I can join chats
- I want to sign in with my credentials
As a logged-in user
- I want to see all available chat rooms
- I want to create a new room with a name
- I want to join a room and see message history
- I want to send messages that appear instantly for others
- I want to see who else is in the room
- I want to see new messages in real-time without refreshing
- I want to be notified when users join/leave the room
- I want to leave a room and return to the room list
- I want to sign out when I'm done
Technical Requirements
Rails Version
- Rails 7.2+
- Ruby 3.2+
Dependencies
-
bcrypt- password hashing -
redis- Action Cable adapter -
tailwindcss-rails- styling -
turbo-rails- Hotwire Turbo -
stimulus-rails- JavaScript framework
Database
- PostgreSQL 14+
- Proper indexes on foreign keys
- Timestamps on all tables
Action Cable
- Redis adapter for production
- Async adapter for development/test
- Authenticated cable connections
- Channel-based messaging
Browser Support
- Modern evergreen browsers (Chrome, Firefox, Safari, Edge)
- WebSocket support required
- JavaScript enabled
Success Criteria
MVP is complete when:
- ✅ Users can sign up and sign in
- ✅ Users can create and join rooms
- ✅ Messages appear in real-time via WebSocket
- ✅ Message history loads when joining
- ✅ User presence is visible (who's online)
- ✅ UI is responsive on mobile and desktop
- ✅ No major bugs or crashes
- ✅ WebSocket reconnects automatically on disconnect
Quality Metrics
- Page load time < 2 seconds
- Message delivery < 100ms
- Zero data loss on disconnect/reconnect
- All user inputs sanitized
- No console errors in browser
Architecture
Architecture
System Overview
┌─────────────────────────────────────────────────────────────┐
│ Client (Browser) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ HTML/CSS │ │ Turbo (JS) │ │ ActionCable │ │
│ │ (Tailwind) │ │ (Hotwire) │ │ (WebSocket) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
HTTP & WebSocket
│
┌─────────────────────────────────────────────────────────────┐
│ Rails Application Server │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Controllers │ │
│ │ • RoomsController • MessagesController │ │
│ │ • UsersController • SessionsController │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Action Cable │ │
│ │ • RoomChannel • PresenceChannel │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Models │ │
│ │ • User • Room • Message │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
┌─────────────────────────┐ ┌─────────────────────────┐
│ PostgreSQL Database │ │ Redis Cache │
│ • users │ │ • Action Cable adapter │
│ • rooms │ │ • WebSocket pub/sub │
│ • messages │ │ • User presence │
└─────────────────────────┘ └─────────────────────────┘
Component Architecture
1. Presentation Layer (Views)
Room List Page
- Display all available rooms
- Show active user count per room
- Create new room form
- Navigation to sign out
Chat Room Page
- Message history (scrollable)
- Message input form
- Send button
- User list sidebar (who's online)
- Leave room button
- Auto-scroll to latest message
Authentication Pages
- Sign up form
- Sign in form
- Simple, minimal design
2. Controller Layer
RoomsController
# Actions:
# - index: list all rooms
# - show: display chat room with messages
# - create: create new room
# - destroy: delete room (optional)
MessagesController
# Actions:
# - create: post new message (broadcasts via cable)
UsersController
# Actions:
# - new: sign up form
# - create: register new user
SessionsController
# Actions:
# - new: sign in form
# - create: authenticate user
# - destroy: sign out
3. Model Layer
User Model
# Attributes: username, password_digest
# Methods: authenticate (via bcrypt)
# Associations: has_many :messages
Room Model
# Attributes: name, description
# Associations: has_many :messages
# Methods: broadcast_message, active_users_count
Message Model
# Attributes: content, user_id, room_id, created_at
# Associations: belongs_to :user, belongs_to :room
# Validations: presence of content
4. Action Cable Layer
RoomChannel
# Purpose: Handle real-time messaging
# Subscription: User subscribes to a room
# Actions:
# - speak: broadcast message to all in room
# - receive: handle incoming messages
PresenceChannel
# Purpose: Track online users
# Actions:
# - appear: user joined room
# - disappear: user left room
# - broadcast_presence: notify all users
Data Flow
Message Sending Flow
1. User types message and clicks Send
│
2. JavaScript captures submit event
│
3. Turbo submits form via fetch (Ajax)
│
4. MessagesController#create receives request
│
5. Create Message record in database
│
6. Broadcast via RoomChannel
│
7. Action Cable pushes to all subscribers
│
8. All clients receive via WebSocket
│
9. Stimulus controller appends message to DOM
│
10. Auto-scroll to bottom
User Presence Flow
1. User joins room (opens chat page)
│
2. JavaScript subscribes to RoomChannel
│
3. Channel broadcasts "user_joined" event
│
4. All subscribers receive notification
│
5. Update online user list in UI
│
6. User leaves (closes page/navigates away)
│
7. WebSocket disconnect triggers "user_left"
│
8. Broadcast to remaining users
│
9. Update user list UI
WebSocket Architecture
Connection Lifecycle
Client Server (Action Cable)
│ │
│─────── Establish WebSocket ──────────→
│ │
│←────── Connection confirmed ─────────│
│ │
│─────── Subscribe to channel ─────────→
│ │
│←────── Subscription confirmed ───────│
│ │
│─────── Send message ──────────────────→
│ │
│←────── Broadcast to all ─────────────│
│ │
│ (message appears for all) │
│ │
│─────── Unsubscribe ───────────────────→
│ │
│─────── Close connection ──────────────→
Redis Pub/Sub
Rails Instance 1 Redis Rails Instance 2
│ │ │
│──── Publish ───────→ │
│ (message) │ │
│ │──── Broadcast ─────→
│ │ │
│ │ │
│←─── Subscribe ─────│ │
│ (to channel) │ │
Security Architecture
Authentication Flow
- User submits username/password
- Controller verifies via bcrypt
- Session created with user_id
- Session cookie stored (encrypted)
- Subsequent requests authenticated via session
WebSocket Security
- Connection identified by session cookie
- Only authenticated users can subscribe
- User can only send to subscribed channels
- Message content sanitized before broadcast
CSRF Protection
- Rails automatic CSRF tokens
- Form submissions include authenticity_token
- WebSocket exempt (session-based auth)
Deployment Architecture
Development
localhost:3000 (Rails)
localhost:6379 (Redis)
PostgreSQL Docker container
Production (Heroku)
Web Dyno (Rails app)
↓
Heroku Redis (Action Cable)
↓
Heroku Postgres (Database)
Technology Stack
Backend
- Ruby 3.2+
- Rails 7.2
- Action Cable (WebSocket)
- bcrypt (password hashing)
- Redis (pub/sub for Action Cable)
Frontend
- Hotwire Turbo (Ajax navigation)
- Stimulus.js (lightweight JS framework)
- Tailwind CSS (utility-first CSS)
- Native browser WebSocket API
Database
- PostgreSQL (primary datastore)
- Redis (WebSocket pub/sub)
Infrastructure
- Docker (local PostgreSQL)
- Heroku or Render (deployment)
- CloudFlare (optional CDN)
Scalability Considerations
Current Capacity (MVP)
- 100+ concurrent users per room
- 1000+ total active connections
- Vertical scaling (larger dyno/server)
Future Scaling (Post-MVP)
- Horizontal scaling with load balancer
- Separate Action Cable servers
- Redis cluster for pub/sub
- Database read replicas
- CDN for static assets
Performance Optimizations
Database
- Indexes on foreign keys (userid, roomid)
- Pagination for message history
- Eager loading to prevent N+1 queries
Caching
- Redis for Action Cable adapter
- HTTP caching headers
- Fragment caching for room list
WebSocket
- Efficient message serialization (JSON)
- Debounce typing indicators (future)
- Batch presence updates
Monitoring & Debugging
Key Metrics
- WebSocket connection count
- Message delivery latency
- Database query time
- Redis memory usage
- Error rate
Tools
- Rails logs (development)
- Heroku logs (production)
- Redis CLI for debugging
- Browser DevTools (Network, Console)
Error Handling
WebSocket Disconnection
- Automatic reconnection attempt
- Show connection status in UI
- Queue messages during disconnection
- Resend on reconnection
Database Errors
- Retry transient failures
- Log errors for debugging
- Show user-friendly error messages
- Graceful degradation
Data Model
Data Model
Entity Relationship Diagram
┌─────────────────────┐
│ users │
├─────────────────────┤
│ id (PK) │
│ username (unique) │
│ password_digest │
│ created_at │
│ updated_at │
└─────────────────────┘
│
│ 1:N (has_many :messages)
│
↓
┌─────────────────────┐ ┌─────────────────────┐
│ messages │ N:1 │ rooms │
├─────────────────────┤─────────├─────────────────────┤
│ id (PK) │ │ id (PK) │
│ content │ │ name (unique) │
│ user_id (FK) │ │ description │
│ room_id (FK) │←────────│ created_at │
│ created_at │ 1:N │ updated_at │
│ updated_at │ └─────────────────────┘
└─────────────────────┘
Table Definitions
users
Stores user authentication information.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | bigint | PRIMARY KEY | Auto-incrementing ID |
| username | string | NOT NULL, UNIQUE | Unique username for login |
| password_digest | string | NOT NULL | Bcrypt hashed password |
| created_at | datetime | NOT NULL | Account creation timestamp |
| updated_at | datetime | NOT NULL | Last update timestamp |
Indexes:
- Primary key on id
- Unique index on username
Validations:
- username: presence, uniqueness, length (3-20 characters)
- password: minimum 6 characters (on virtual attribute)
rooms
Stores chat room information.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | bigint | PRIMARY KEY | Auto-incrementing ID |
| name | string | NOT NULL, UNIQUE | Room display name |
| description | text | Optional room description | |
| created_at | datetime | NOT NULL | Room creation timestamp |
| updated_at | datetime | NOT NULL | Last update timestamp |
Indexes:
- Primary key on id
- Unique index on name
Validations:
- name: presence, uniqueness, length (1-50 characters)
- description: length (maximum 200 characters)
messages
Stores chat messages sent in rooms.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | bigint | PRIMARY KEY | Auto-incrementing ID |
| content | text | NOT NULL | Message content |
| user_id | bigint | NOT NULL, FOREIGN KEY | References users.id |
| room_id | bigint | NOT NULL, FOREIGN KEY | References rooms.id |
| created_at | datetime | NOT NULL | Message sent timestamp |
| updated_at | datetime | NOT NULL | Last update timestamp |
Indexes:
- Primary key on id
- Foreign key index on user_id
- Foreign key index on room_id
- Composite index on (room_id, created_at) for efficient history queries
Validations:
- content: presence, length (1-1000 characters)
- userid: presence
- roomid: presence
Associations:
- belongsto :user
- belongsto :room
Model Associations
User Model
class User < ApplicationRecord
has_secure_password
has_many :messages, dependent: :destroy
validates :username, presence: true,
uniqueness: true,
length: { in: 3..20 }
validates :password, length: { minimum: 6 }, allow_nil: true
end
Room Model
class Room < ApplicationRecord
has_many :messages, dependent: :destroy
validates :name, presence: true,
uniqueness: true,
length: { in: 1..50 }
validates :description, length: { maximum: 200 }
# Returns the 100 most recent messages
def recent_messages(limit = 100)
messages.includes(:user).order(created_at: :desc).limit(limit).reverse
end
end
Message Model
class Message < ApplicationRecord
belongs_to :user
belongs_to :room
validates :content, presence: true, length: { in: 1..1000 }
# Broadcast message to room subscribers
after_create_commit do
broadcast_append_to(
"room_#{room_id}",
target: "messages",
partial: "messages/message",
locals: { message: self }
)
end
end
Database Migrations
Create Users Table
class CreateUsers < ActiveRecord::Migration[7.2]
def change
create_table :users do |t|
t.string :username, null: false
t.string :password_digest, null: false
t.timestamps
end
add_index :users, :username, unique: true
end
end
Create Rooms Table
class CreateRooms < ActiveRecord::Migration[7.2]
def change
create_table :rooms do |t|
t.string :name, null: false
t.text :description
t.timestamps
end
add_index :rooms, :name, unique: true
end
end
Create Messages Table
class CreateMessages < ActiveRecord::Migration[7.2]
def change
create_table :messages do |t|
t.text :content, null: false
t.references :user, null: false, foreign_key: true
t.references :room, null: false, foreign_key: true
t.timestamps
end
add_index :messages, [:room_id, :created_at]
end
end
Sample Data (Seeds)
# Create sample users
alice = User.create!(
username: "alice",
password: "password123"
)
bob = User.create!(
username: "bob",
password: "password123"
)
# Create sample rooms
general = Room.create!(
name: "General",
description: "General discussion room"
)
random = Room.create!(
name: "Random",
description: "Random topics and off-topic chat"
)
rails = Room.create!(
name: "Rails Help",
description: "Get help with Ruby on Rails development"
)
# Create sample messages
Message.create!(
user: alice,
room: general,
content: "Welcome to the General chat room!"
)
Message.create!(
user: bob,
room: general,
content: "Thanks! Happy to be here."
)
Query Patterns
Common Queries
Get all rooms with message count:
ruby
Room.left_joins(:messages)
.group(:id)
.select('rooms.*, COUNT(messages.id) as messages_count')
.order(created_at: :desc)
Get recent messages for a room:
ruby
room.messages
.includes(:user)
.order(created_at: :desc)
.limit(100)
.reverse
Get user's message count:
ruby
user.messages.count
Find room by name:
ruby
Room.find_by(name: "General")
Performance Considerations
Indexes Strategy
- Primary keys: Automatic B-tree index for fast lookups
- Foreign keys: Indexed for efficient joins
- Unique constraints: Username and room name for data integrity
- Composite index: (roomid, createdat) for paginated message queries
N+1 Query Prevention
# ❌ Bad - N+1 queries
messages.each { |m| puts m.user.username }
# ✅ Good - Eager loading
messages.includes(:user).each { |m| puts m.user.username }
Pagination
# Use kaminari or pagy gem
messages.order(created_at: :desc).page(params[:page]).per(50)
Data Integrity
Foreign Key Constraints
-
messages.user_id→users.id(CASCADE on delete) -
messages.room_id→rooms.id(CASCADE on delete)
Dependent Destroy
- When a user is deleted, all their messages are deleted
- When a room is deleted, all its messages are deleted
Validations
- Username uniqueness at database and application level
- Room name uniqueness at database and application level
- Message content presence
- Foreign key presence
Future Enhancements
Additional Tables (Post-MVP)
room_memberships (for private rooms)
ruby
create_table :room_memberships do |t|
t.references :user, null: false, foreign_key: true
t.references :room, null: false, foreign_key: true
t.string :role, default: "member" # member, moderator, admin
t.timestamps
end
reactions (for message reactions)
ruby
create_table :reactions do |t|
t.references :user, null: false, foreign_key: true
t.references :message, null: false, foreign_key: true
t.string :emoji, null: false
t.timestamps
end
attachments (for file uploads)
ruby
create_table :active_storage_attachments do |t|
# Rails Active Storage tables
end
User Flows
User Flows
Overview
This document describes the main user interaction flows for the chat application.
Flow 1: User Registration
┌──────────────────────────────────────────────────────────────┐
│ 1. Visit Homepage (not logged in) │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 2. Click "Sign Up" button │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 3. Fill out registration form: │
│ - Username (3-20 characters) │
│ - Password (min 6 characters) │
│ - Password confirmation │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 4. Submit form │
└───────────────────────────────┬──────────────────────────────┘
│
┌───────────┴───────────┐
│ │
↓ ↓
┌─────────────────────┐ ┌─────────────────────┐
│ Validation fails │ │ Validation succeeds │
│ Show errors │ │ Create user │
│ Stay on form │ │ Log in user │
└───────┬──────────────┘ └───────┬─────────────┘
│ │
│ ↓
│ ┌─────────────────────┐
│ │ Redirect to │
│ │ Room list page │
│ └─────────────────────┘
│ │
└─────────────────────────┘
Success Criteria:
- User account created in database
- Password securely hashed with bcrypt
- User automatically logged in
- Session cookie set
- Redirected to room list
Flow 2: User Login
┌──────────────────────────────────────────────────────────────┐
│ 1. Visit Homepage or click "Sign In" │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 2. Fill out login form: │
│ - Username │
│ - Password │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 3. Submit form │
└───────────────────────────────┬──────────────────────────────┘
│
┌───────────┴───────────┐
│ │
↓ ↓
┌─────────────────────┐ ┌─────────────────────┐
│ Invalid credentials │ │ Valid credentials │
│ Show error message │ │ Set session │
│ Stay on login page │ │ Log in user │
└───────┬──────────────┘ └───────┬─────────────┘
│ │
│ ↓
│ ┌─────────────────────┐
│ │ Redirect to │
│ │ Room list page │
│ └─────────────────────┘
│ │
└─────────────────────────┘
Success Criteria:
- User authenticated via bcrypt
- Session cookie created
- Redirected to room list
- Username displayed in navbar
Flow 3: Browse and Join Chat Room
┌──────────────────────────────────────────────────────────────┐
│ 1. User on Room List page (logged in) │
│ - See all available rooms │
│ - See room names, descriptions │
│ - See active user counts │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 2. Click on a room to join │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 3. Navigate to Room Show page │
│ - Load message history (last 100 messages) │
│ - Establish WebSocket connection │
│ - Subscribe to RoomChannel │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 4. User sees: │
│ - Room name and description │
│ - Message history │
│ - List of online users │
│ - Message input box │
│ - "Leave Room" button │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 5. Broadcast "user_joined" event │
│ - Notify all other users in room │
│ - Update online user list for everyone │
└──────────────────────────────────────────────────────────────┘
Success Criteria:
- WebSocket connection established
- User subscribed to room channel
- Message history loaded
- User appears in online list
- Join notification visible to others
Flow 4: Send and Receive Messages
┌──────────────────────────────────────────────────────────────┐
│ 1. User in chat room │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 2. Type message in input box │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 3. Click "Send" or press Enter │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 4. Client sends via Turbo form submit │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 5. Server (MessagesController#create): │
│ - Validate message │
│ - Save to database │
│ - Broadcast via Action Cable │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 6. Action Cable broadcasts to all subscribers │
│ - Message sent via RoomChannel │
│ - All connected clients receive │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 7. Each client receives message: │
│ - Append message to chat window │
│ - Show username and timestamp │
│ - Auto-scroll to bottom │
│ - Clear input box (for sender) │
└──────────────────────────────────────────────────────────────┘
Real-time Flow:
User A Server User B
│ │ │
│──── Send message ─────────→│ │
│ │ │
│ │──── Broadcast ─────────→│
│ │ │
│←─── Message appears ───────│ │
│ │ │
│ │ │
│ Message appears ───────────────────→│
Success Criteria:
- Message saved to database
- Message appears instantly for all users
- Sender's input cleared
- Auto-scroll to latest message
- Timestamp displayed
- Sender username shown
Flow 5: Leave Chat Room
┌──────────────────────────────────────────────────────────────┐
│ 1. User clicks "Leave Room" button │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 2. WebSocket unsubscribes from RoomChannel │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 3. Broadcast "user_left" event │
│ - Notify remaining users │
│ - Update online user list │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 4. Navigate back to Room List │
└──────────────────────────────────────────────────────────────┘
Success Criteria:
- User unsubscribed from channel
- Leave notification sent to others
- User removed from online list
- Redirected to room list
Flow 6: Create New Chat Room
┌──────────────────────────────────────────────────────────────┐
│ 1. User on Room List page │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 2. Click "Create New Room" button │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 3. Fill out new room form: │
│ - Room name (required, 1-50 chars) │
│ - Description (optional, max 200 chars) │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 4. Submit form │
└───────────────────────────────┬──────────────────────────────┘
│
┌───────────┴───────────┐
│ │
↓ ↓
┌─────────────────────┐ ┌─────────────────────┐
│ Validation fails │ │ Validation succeeds │
│ (name taken/invalid) │ │ Create room │
│ Show errors │ │ Save to database │
└───────┬──────────────┘ └───────┬─────────────┘
│ │
│ ↓
│ ┌─────────────────────┐
│ │ Redirect to new │
│ │ room's chat page │
│ │ User auto-joins │
│ └─────────────────────┘
│ │
└─────────────────────────┘
Success Criteria:
- Room created in database
- Room appears in room list
- Creator automatically joins room
- Room accessible to all users
Flow 7: User Logout
┌──────────────────────────────────────────────────────────────┐
│ 1. User clicks "Sign Out" in navbar │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 2. Submit logout form (DELETE request) │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 3. Server destroys session │
│ - Clear session cookie │
│ - WebSocket disconnects │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 4. Redirect to sign in page │
└───────────────────────────────┬──────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 5. Show flash message: "Signed out successfully" │
└──────────────────────────────────────────────────────────────┘
Success Criteria:
- Session cleared
- User no longer authenticated
- WebSocket disconnected
- Redirected to login page
Flow 8: Real-time Presence Updates
User A (Browser) Server User B (Browser)
│ │ │
│──── Join room ───────────→ │
│ │ │
│ │─── Broadcast joined ─────→│
│ │ │
│ │ Update user list
│ │ │
│ │←──── User B joins ────────│
│ │ │
│←─── Broadcast joined ────│ │
│ │ │
Update user list │ │
│ │ │
│ │ │
│──── Leave room ──────────→ │
│ │ │
│ │─── Broadcast left ───────→│
│ │ │
│ │ Update user list
Success Criteria:
- Join events broadcast in real-time
- Leave events broadcast in real-time
- User list updated automatically
- No page refresh required
Error Handling Flows
Connection Lost Flow
1. User loses internet connection
2. WebSocket disconnects
3. Show "Connection lost" indicator
4. Attempt automatic reconnection
5. On success: "Connected" indicator
6. On failure: "Reconnecting..." with manual retry button
Message Send Failure Flow
1. User sends message
2. Network error or validation failure
3. Show error message
4. Keep message in input box
5. Allow user to retry
Invalid Session Flow
1. User's session expires
2. Attempt to access protected page
3. Redirect to sign in page
4. Show flash: "Please sign in to continue"
5. After login, redirect to originally requested page
Navigation Map
┌──────────────────┐
│ Sign In Page │
└────────┬─────────┘
│
┌─────────────┴─────────────┐
│ │
↓ ↓
┌──────────────────┐ ┌──────────────────┐
│ Sign Up Page │ │ Room List Page │
└──────────────────┘ └────────┬─────────┘
│ │
└────────────────┬───────┘
│
↓
┌─────────────────────────┐
│ Chat Room Page │
│ (with WebSocket) │
└─────────────────────────┘
│
│ Leave Room
│
↓
┌─────────────────────────┐
│ Back to Room List │
└─────────────────────────┘
Mobile Considerations
Touch Interactions
- Large tap targets (min 44x44px)
- Swipe to scroll message history
- Pull-to-refresh for room list
- Touch-friendly send button
Mobile-specific Flows
- Bottom-anchored message input (above keyboard)
- Collapsible user list sidebar
- Compact message layout
- Auto-hide header on scroll
Accessibility Considerations
Keyboard Navigation
- Tab through all interactive elements
- Enter to send message
- Escape to close modals
- Arrow keys to navigate messages
Screen Reader Support
- ARIA labels on buttons
- Message announcements
- Status indicators
- Focus management
Agent Usage
Agent Usage Guide
Overview
This guide explains how AI agents (like Claude, GPT-4, Gemini) should approach implementing this Rails chat application blueprint.
Before Starting
1. Read Documents in Order
Read the blueprint documentation in this sequence:
1. README.md - Overview and quick start
2. 01_requirements.md - What needs to be built
3. 02_architecture.md - System design and components
4. 03datamodel.md - Database schema
5. 04userflows.md - User interactions
6. This file (05agentusage.md) - Implementation guidance
7. 06_tasks.md - Step-by-step tasks
8. 99agentnotes.md - Tips and gotchas
2. Confirm Environment
Verify the following are available:
- Ruby 3.2+ installed
- Rails 7.2+ gem installed
- PostgreSQL running (Docker container)
- Redis running (for Action Cable)
- Node.js 18+ (for JavaScript)
- VS Code editor
3. Understand the Context
- This is a real-time chat application
- Uses Action Cable for WebSocket communication
- Focus on simplicity over feature completeness
- Target: MVP in 4-6 hours
Implementation Strategy
Phase 1: Foundation (Task 1-2)
Goal: Get Rails app running with authentication
- Create new Rails app with proper configuration
- Set up PostgreSQL connection
- Configure Redis for Action Cable
- Add required gems
- Implement user authentication (bcrypt)
- Create sign up / sign in / sign out flows
Checkpoint: Users can register and log in
Phase 2: Core Models (Task 3-5)
Goal: Database and models ready
- Generate models (User, Room, Message)
- Set up associations
- Add validations
- Create seed data
- Test model relationships in console
Checkpoint: Can create rooms and messages in Rails console
Phase 3: Room Management (Task 6-7)
Goal: Users can browse and join rooms
- Create RoomsController
- Build room list view
- Build room show view (chat page)
- Add room creation form
- Style with Tailwind CSS
Checkpoint: Users can see and create rooms (no real-time yet)
Phase 4: Real-time Chat (Task 8-9)
Goal: WebSocket messaging works
- Generate RoomChannel
- Configure Action Cable
- Create Stimulus controller for chat
- Implement message broadcasting
- Test WebSocket connection
Checkpoint: Messages appear in real-time across browsers
Phase 5: User Presence (Task 10)
Goal: Show who's online
- Track user subscriptions
- Broadcast join/leave events
- Update UI with online users
- Test presence detection
Checkpoint: User list updates in real-time
Phase 6: Polish (Task 11-12)
Goal: Production-ready features
- Auto-scroll to latest message
- Empty states
- Loading indicators
- Error handling
- Mobile responsiveness
- Final testing
Checkpoint: App works smoothly on mobile and desktop
Working with Action Cable
Key Concepts
1. Channels are like WebSocket endpoints
```ruby
Think of channels as controllers for WebSocket
class RoomChannel < ApplicationCable::Channel
def subscribed
# User connects to a room
streamfrom "room#{params[:room_id]}"
end
def unsubscribed
# User disconnects
end
end
```
2. Broadcasting sends data to subscribers
```ruby
In your controller or model
ActionCable.server.broadcast(
"room#{roomid}",
message: "Hello!"
)
```
3. Client receives via JavaScript
javascript
// Stimulus controller
consumer.subscriptions.create(
{ channel: "RoomChannel", room_id: roomId },
{
received(data) {
// Handle incoming message
this.appendMessage(data)
}
}
)
Common Patterns
Pattern 1: Message Broadcasting
```ruby
After creating a message
message.broadcastappendto(
"room#{roomid}",
target: "messages",
partial: "messages/message"
)
```
Pattern 2: Presence Tracking
```ruby
Track connected users in Redis
def subscribed
currentuser.appearinroom(params[:roomid])
streamfrom "room#{params[:room_id]}"
end
def unsubscribed
currentuser.disappearfromroom(params[:roomid])
end
```
Pattern 3: Authorized Subscriptions
```ruby
Only logged-in users can subscribe
class ApplicationCable::Connection < ActionCable::Connection::Base
identifiedby :currentuser
def connect
self.currentuser = findverified_user
end
private
def findverifieduser
if verifieduser = User.findby(id: cookies.encrypted[:userid])
verifieduser
else
rejectunauthorizedconnection
end
end
end
```
Code Organization
File Structure
app/
├── channels/
│ ├── application_cable/
│ │ ├── channel.rb
│ │ └── connection.rb
│ └── room_channel.rb
├── controllers/
│ ├── application_controller.rb
│ ├── rooms_controller.rb
│ ├── messages_controller.rb
│ ├── users_controller.rb
│ └── sessions_controller.rb
├── models/
│ ├── user.rb
│ ├── room.rb
│ └── message.rb
├── views/
│ ├── layouts/
│ │ └── application.html.erb
│ ├── rooms/
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ ├── messages/
│ │ └── _message.html.erb
│ ├── users/
│ │ └── new.html.erb
│ └── sessions/
│ └── new.html.erb
└── javascript/
├── controllers/
│ └── room_controller.js
└── channels/
├── consumer.js
└── room_channel.js
Controller Responsibilities
RoomsController
- index: List all rooms
- show: Display chat room
- create: Create new room
MessagesController
- create: Post new message (broadcasts via cable)
UsersController
- new: Sign up form
- create: Register user
SessionsController
- new: Sign in form
- create: Log in user
- destroy: Log out user
Testing Strategy
Manual Testing Checklist
Authentication:
- [ ] Can sign up with valid credentials
- [ ] Cannot sign up with invalid data
- [ ] Can sign in with correct password
- [ ] Cannot sign in with wrong password
- [ ] Can sign out
- [ ] Protected pages redirect when not logged in
Room Management:
- [ ] Can view room list
- [ ] Can create new room
- [ ] Cannot create room with duplicate name
- [ ] Can navigate to room
Real-time Messaging:
- [ ] Open room in 2+ browsers
- [ ] Send message in one browser
- [ ] Message appears in all browsers instantly
- [ ] Username and timestamp displayed
- [ ] Message persists after page refresh
User Presence:
- [ ] User appears in online list when joining
- [ ] User disappears when leaving
- [ ] Join notification visible to others
- [ ] Leave notification visible to others
UI/UX:
- [ ] Works on mobile screen sizes
- [ ] Auto-scrolls to latest message
- [ ] Input clears after sending
- [ ] Loading states visible
- [ ] Error messages displayed
Multi-window Testing
# Terminal 1: Start Rails
rails server
# Terminal 2: Start Redis
redis-server
# Browser 1: Sign in as Alice
# Browser 2: Sign in as Bob (incognito mode)
# Both join the same room and chat
Common Pitfalls
1. Redis Not Running
Problem: WebSocket connects but messages don't broadcast
Solution: Ensure Redis is running (redis-server)
2. Action Cable Not Configured
Problem: Cable connection fails
Solution: Check config/cable.yml has correct Redis URL
3. CSRF Token Issues
Problem: Form submissions fail
Solution: Ensure <%= csrf_meta_tags %> in layout
4. N+1 Queries
Problem: Slow message loading
Solution: Use .includes(:user) when loading messages
5. WebSocket Origin Errors
Problem: Cable rejected in production
Solution: Configure config.action_cable.allowed_request_origins
6. Memory Leaks
Problem: Multiple subscriptions created
Solution: Unsubscribe when leaving room
Debugging Tips
Check WebSocket Connection
// In browser console
App.cable.connection.webSocket.readyState
// 1 = OPEN, 0 = CONNECTING, 2 = CLOSING, 3 = CLOSED
Monitor Action Cable
# Rails console
ActionCable.server.connections.size
# Shows number of active connections
Redis Debugging
# Redis CLI
redis-cli
> KEYS *
> GET action_cable:...
Rails Console Testing
# Test broadcasting manually
ActionCable.server.broadcast("room_1", { message: "test" })
# Check subscriptions
ActionCable.server.pubsub.send(:redis_connection).pubsub("channels", "*")
Performance Optimization
Database
- Add indexes on foreign keys
- Eager load associations (
.includes(:user)) - Limit message history (last 100 messages)
Action Cable
- Use Redis adapter (not async in production)
- Set proper connection pool size
- Configure timeout settings
Frontend
- Debounce scroll events
- Lazy load message history
- Use Turbo for fast navigation
Deployment Considerations
Heroku
# Add Redis addon
heroku addons:create heroku-redis:mini
# Set Action Cable URL
heroku config:set ACTION_CABLE_URL=wss://yourapp.herokuapp.com/cable
Environment Variables
# .env file (use dotenv gem)
DATABASE_URL=postgresql://...
REDIS_URL=redis://localhost:6379/0
ACTION_CABLE_ALLOWED_ORIGINS=http://localhost:3000
Production Checklist
- [ ] Redis addon provisioned
- [ ] Action Cable URL configured
- [ ] Allowed origins set
- [ ] Database migrations run
- [ ] Seed data loaded
- [ ] WebSocket working across browsers
Agent Workflow
Step-by-Step Process
-
Read all documentation (15 min)
- Understand requirements and architecture
- Note key features and constraints
-
Set up Rails project (30 min)
- Create app with proper config
- Add gems
- Configure database and Redis
-
Implement authentication (45 min)
- User model with bcrypt
- Sign up / sign in / sign out
-
Build core models (30 min)
- Room and Message models
- Associations and validations
- Seed data
-
Create room views (45 min)
- Room list page
- Room show page (chat)
- Forms and navigation
-
Add Action Cable (60 min)
- Generate channel
- Configure connection
- Implement broadcasting
- JavaScript subscription
-
Implement presence (45 min)
- Track join/leave
- Update user list
- Notifications
-
Polish and test (45 min)
- Auto-scroll
- Mobile responsive
- Error handling
- Multi-browser testing
Total: 4-6 hours
Questions to Ask User
Before Starting
- Do you have PostgreSQL running?
- Do you have Redis installed?
- Should we create a new Rails app or add to existing?
- Any specific room features needed?
During Implementation
- Does the authentication flow work for you?
- Can you test in multiple browsers?
- Is the UI responsive enough on mobile?
After Completion
- Should we add direct messages?
- Want file upload support?
- Need typing indicators?
Success Criteria
Definition of Done
A task is complete when:
1. ✅ Code written and working
2. ✅ No console errors
3. ✅ Tested manually
4. ✅ UI looks good
5. ✅ Real-time features work across browsers
MVP Complete When
- [ ] Users can sign up and log in
- [ ] Users can create and join rooms
- [ ] Messages broadcast in real-time
- [ ] User presence visible
- [ ] No major bugs
- [ ] Mobile responsive
- [ ] WebSocket reconnects gracefully
Next Steps After MVP
Phase 2 Features
- Direct messaging (DM)
- File uploads
- Message editing/deletion
- Typing indicators
- Read receipts
Phase 3 Features
- User profiles with avatars
- Private rooms
- Room moderation
- Search messages
- Email notifications
Scaling
- Separate Action Cable servers
- Redis cluster
- Database read replicas
- CDN for assets
Tasks
Implementation Tasks
Follow these tasks sequentially to build the Rails chat application with Action Cable.
Task 1: Project Setup
Create Rails Application
rails new chat-app \
--database=postgresql \
--css=tailwind \
--javascript=importmap
Configure Database
Edit config/database.yml:
yaml
development:
adapter: postgresql
encoding: unicode
database: chat_app_development
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: postgres
password: postgres
host: database-server-dev1
Add Required Gems
Edit Gemfile:
```ruby
gem 'bcrypt', '~> 3.1.7' # Uncomment if commented
gem 'redis', '~> 5.0'
group :development, :test do
gem 'debug', platforms: %i[ mri windows ]
end
```
Run:
bash
bundle install
Configure Redis for Action Cable
Edit config/cable.yml:
```yaml
development:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
test:
adapter: test
production:
adapter: redis
url: <%= ENV.fetch("REDISURL") { "redis://localhost:6379/1" } %>
channelprefix: chatappproduction
```
Setup Database
rails db:create
rails db:migrate
Test Server
./bin/dev # Or rails server
Visit http://localhost:3000 - you should see Rails welcome page.
✅ Checkpoint: Rails app running with PostgreSQL and Redis configured
Task 2: User Authentication
Generate User Model
rails generate model User username:string password_digest:string
rails db:migrate
Configure User Model
Edit app/models/user.rb:
```ruby
class User < ApplicationRecord
hassecurepassword
validates :username, presence: true,
uniqueness: true,
length: { in: 3..20 }
validates :password, length: { minimum: 6 }, allow_nil: true
end
```
Create Sessions Controller
rails generate controller Sessions new create destroy
Edit app/controllers/sessions_controller.rb:
```ruby
class SessionsController < ApplicationController
def new
# Sign in form
end
def create
user = User.findby(username: params[:username])
if user&.authenticate(params[:password])
session[:userid] = user.id
redirectto roomspath, notice: "Signed in successfully!"
else
flash.now[:alert] = "Invalid username or password"
render :new, status: :unprocessable_entity
end
end
def destroy
session[:userid] = nil
redirectto signinpath, notice: "Signed out successfully!"
end
end
```
Create Users Controller
rails generate controller Users new create
Edit app/controllers/users_controller.rb:
```ruby
class UsersController < ApplicationController
def new
@user = User.new
end
def create
@user = User.new(userparams)
if @user.save
session[:userid] = @user.id
redirectto roomspath, notice: "Account created successfully!"
else
render :new, status: :unprocessable_entity
end
end
private
def userparams
params.require(:user).permit(:username, :password, :passwordconfirmation)
end
end
```
Add Authentication Helpers
Edit app/controllers/application_controller.rb:
```ruby
class ApplicationController < ActionController::Base
helpermethod :currentuser, :logged_in?
def currentuser
@currentuser ||= User.findby(id: session[:userid]) if session[:user_id]
end
def loggedin?
currentuser.present?
end
def requirelogin
unless loggedin?
flash[:alert] = "You must be logged in to access this page"
redirectto signin_path
end
end
end
```
Create Routes
Edit config/routes.rb:
```ruby
Rails.application.routes.draw do
root "sessions#new"
get "signin", to: "sessions#new"
post "signin", to: "sessions#create"
delete "sign_out", to: "sessions#destroy"
get "signup", to: "users#new"
post "signup", to: "users#create"
resources :rooms, only: [:index, :show, :create] do
resources :messages, only: [:create]
end
end
```
Create Sign In View
Create app/views/sessions/new.html.erb:
```erb
Sign In
<%= formwith url: signinpath, method: :post, local: true do |f| %>
<%= f.label :username, class: "block mb-2" %>
<%= f.textfield :username, required: true,
class: "w-full px-4 py-2 border rounded" %>
<div class="mb-4">
<%= f.label :password, class: "block mb-2" %>
<%= f.password_field :password, required: true,
class: "w-full px-4 py-2 border rounded" %>
</div>
<%= f.submit "Sign In", class: "w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" %>
<% end %>
Don't have an account? <%= linkto "Sign Up", signup_path, class: "text-blue-500" %>
```
Create Sign Up View
Create app/views/users/new.html.erb:
```erb
Sign Up
<%= formwith model: @user, url: signuppath, local: true do |f| %>
<% if @user.errors.any? %>
<% @user.errors.fullmessages.each do |message| %>
<%= message %>
<% end %>
<% end %>
<div class="mb-4">
<%= f.label :username, class: "block mb-2" %>
<%= f.text_field :username, required: true,
class: "w-full px-4 py-2 border rounded" %>
</div>
<div class="mb-4">
<%= f.label :password, class: "block mb-2" %>
<%= f.password_field :password, required: true,
class: "w-full px-4 py-2 border rounded" %>
</div>
<div class="mb-4">
<%= f.label :password_confirmation, class: "block mb-2" %>
<%= f.password_field :password_confirmation, required: true,
class: "w-full px-4 py-2 border rounded" %>
</div>
<%= f.submit "Sign Up", class: "w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" %>
<% end %>
Already have an account? <%= linkto "Sign In", signin_path, class: "text-blue-500" %>
```
Update Application Layout
Edit app/views/layouts/application.html.erb:
```erb
<!DOCTYPE html>
ChatApp
<%= csrfmetatags %>
<%= cspmetatag %>
<%= stylesheetlinktag "tailwind", "inter-font", "data-turbo-track": "reload" %>
<%= stylesheetlinktag "application", "data-turbo-track": "reload" %>
<%= javascriptimportmaptags %>
<% if loggedin? %>
💬 Chat App
👤 <%= currentuser.username %>
<%= buttonto "Sign Out", signout_path, method: :delete,
class: "bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600" %>
<% end %>
<% if flash[:notice] %>
<div class="container mx-auto px-4 mb-4">
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
<%= flash[:notice] %>
</div>
</div>
<% end %>
<% if flash[:alert] %>
<div class="container mx-auto px-4 mb-4">
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
<%= flash[:alert] %>
</div>
</div>
<% end %>
<%= yield %>
```
✅ Checkpoint: Users can sign up and sign in
Task 3: Create Room and Message Models
Generate Room Model
rails generate model Room name:string description:text
Generate Message Model
rails generate model Message content:text user:references room:references
Add Index to Messages
Edit the migration file for messages and add:
ruby
add_index :messages, [:room_id, :created_at]
Run Migrations
rails db:migrate
Configure Room Model
Edit app/models/room.rb:
```ruby
class Room < ApplicationRecord
has_many :messages, dependent: :destroy
validates :name, presence: true,
uniqueness: true,
length: { in: 1..50 }
validates :description, length: { maximum: 200 }
def recentmessages(limit = 100)
messages.includes(:user).order(createdat: :desc).limit(limit).reverse
end
end
```
Configure Message Model
Edit app/models/message.rb:
```ruby
class Message < ApplicationRecord
belongsto :user
belongsto :room
validates :content, presence: true, length: { in: 1..1000 }
aftercreatecommit do
broadcastappendto(
"room#{roomid}",
target: "messages",
partial: "messages/message",
locals: { message: self }
)
end
end
```
Update User Model
Edit app/models/user.rb and add:
ruby
has_many :messages, dependent: :destroy
✅ Checkpoint: Models created with associations
Task 4: Create Seed Data
Edit db/seeds.rb:
```ruby
Clear existing data
Message.destroyall
Room.destroyall
User.destroy_all
Create users
alice = User.create!(username: "alice", password: "password123", passwordconfirmation: "password123")
bob = User.create!(username: "bob", password: "password123", passwordconfirmation: "password123")
puts "Created users: alice, bob (password: password123)"
Create rooms
general = Room.create!(name: "General", description: "General discussion room")
random = Room.create!(name: "Random", description: "Random topics and off-topic chat")
rails_help = Room.create!(name: "Rails Help", description: "Get help with Ruby on Rails")
puts "Created rooms: General, Random, Rails Help"
Create sample messages
Message.create!(user: alice, room: general, content: "Welcome to the General chat room!")
Message.create!(user: bob, room: general, content: "Thanks! Happy to be here.")
Message.create!(user: alice, room: rails_help, content: "Anyone have experience with Action Cable?")
puts "Created sample messages"
puts "✅ Seed data complete!"
```
Run seeds:
bash
rails db:seed
✅ Checkpoint: Database has sample data
Task 5: Create Rooms Controller and Views
Generate Rooms Controller
rails generate controller Rooms index show create
Configure Rooms Controller
Edit app/controllers/rooms_controller.rb:
```ruby
class RoomsController < ApplicationController
beforeaction :requirelogin
def index
@rooms = Room.all.order(created_at: :desc)
@room = Room.new
end
def show
@room = Room.find(params[:id])
@messages = @room.recent_messages
@message = Message.new
end
def create
@room = Room.new(roomparams)
if @room.save
redirectto @room, notice: "Room created successfully!"
else
@rooms = Room.all
render :index, status: :unprocessable_entity
end
end
private
def room_params
params.require(:room).permit(:name, :description)
end
end
```
Create Room Index View
Create app/views/rooms/index.html.erb:
```erb
<!-- Room List -->
Chat Rooms
<div class="grid gap-4">
<% @rooms.each do |room| %>
<%= link_to room, class: "block bg-white p-6 rounded-lg shadow hover:shadow-lg transition" do %>
<h3 class="text-xl font-bold mb-2"><%= room.name %></h3>
<p class="text-gray-600"><%= room.description %></p>
<p class="text-sm text-gray-500 mt-2">
<%= room.messages.count %> messages
</p>
<% end %>
<% end %>
</div>
</div>
<!-- Create Room Form -->
<div>
<h2 class="text-2xl font-bold mb-4">Create Room</h2>
<%= form_with model: @room, url: rooms_path, local: true, class: "bg-white p-6 rounded-lg shadow" do |f| %>
<% if @room.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<ul>
<% @room.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="mb-4">
<%= f.label :name, class: "block mb-2 font-bold" %>
<%= f.text_field :name, required: true,
class: "w-full px-4 py-2 border rounded" %>
</div>
<div class="mb-4">
<%= f.label :description, class: "block mb-2 font-bold" %>
<%= f.text_area :description, rows: 3,
class: "w-full px-4 py-2 border rounded" %>
</div>
<%= f.submit "Create Room", class: "w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" %>
<% end %>
</div>
```
Create Room Show View
Create app/views/rooms/show.html.erb:
```erb
<!-- Room Header -->
<%= @room.name %>
<%= @room.description %>
<%= linkto "← Back to Rooms", roomspath,
class: "text-blue-500 hover:text-blue-700" %>
<!-- Messages Container -->
<div id="messages" class="p-4 h-96 overflow-y-auto space-y-3">
<%= render @messages %>
</div>
<!-- Message Form -->
<div class="border-t p-4">
<%= form_with model: [@room, @message],
local: false,
data: { controller: "reset-form", action: "turbo:submit-end->reset-form#reset" } do |f| %>
<div class="flex space-x-2">
<%= f.text_area :content,
rows: 2,
placeholder: "Type your message...",
required: true,
class: "flex-1 px-4 py-2 border rounded resize-none" %>
<%= f.submit "Send",
class: "bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600" %>
</div>
<% end %>
</div>
```
Create Message Partial
Create app/views/messages/_message.html.erb:
erb
<div id="<%= dom_id(message) %>" class="flex items-start space-x-3">
<div class="flex-1 bg-gray-100 rounded-lg p-3">
<div class="flex items-baseline space-x-2 mb-1">
<span class="font-bold text-sm"><%= message.user.username %></span>
<span class="text-xs text-gray-500">
<%= message.created_at.strftime("%I:%M %p") %>
</span>
</div>
<p class="text-gray-800"><%= message.content %></p>
</div>
</div>
✅ Checkpoint: Can view rooms and see message history
Task 6: Create Messages Controller
Create app/controllers/messages_controller.rb:
```ruby
class MessagesController < ApplicationController
beforeaction :requirelogin
def create
@room = Room.find(params[:roomid])
@message = @room.messages.build(messageparams)
@message.user = current_user
if @message.save
# Message will be broadcast automatically via after_create_commit
head :ok
else
render json: { errors: @message.errors.full_messages },
status: :unprocessable_entity
end
end
private
def message_params
params.require(:message).permit(:content)
end
end
```
✅ Checkpoint: Can post messages (not real-time yet)
Task 7: Configure Action Cable
Create Room Channel
rails generate channel Room
Configure Room Channel
Edit app/channels/room_channel.rb:
```ruby
class RoomChannel < ApplicationCable::Channel
def subscribed
room = Room.find(params[:roomid])
streamfor room
end
def unsubscribed
# Cleanup when channel is unsubscribed
end
end
```
Configure Cable Connection
Edit app/channels/application_cable/connection.rb:
```ruby
module ApplicationCable
class Connection < ActionCable::Connection::Base
identifiedby :currentuser
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
```
Create Stimulus Controller
Create app/javascript/controllers/room_controller.js:
```javascript
import { Controller } from "@hotwired/stimulus"
import { createConsumer } from "@rails/actioncable"
export default class extends Controller {
static values = { roomId: Number }
connect() {
this.channel = createConsumer().subscriptions.create(
{ channel: "RoomChannel", roomid: this.roomIdValue },
{
connected: this.connected.bind(this),
disconnected: this.disconnected.bind(this),
received: this.received.bind(this)
}
)
this.scrollToBottom()
}
disconnect() {
this.channel.unsubscribe()
}
_connected() {
console.log("Connected to room channel")
}
_disconnected() {
console.log("Disconnected from room channel")
}
_received(data) {
// Turbo Stream will handle the append
this.scrollToBottom()
}
scrollToBottom() {
const messages = document.getElementById("messages")
if (messages) {
messages.scrollTop = messages.scrollHeight
}
}
}
```
Create Reset Form Controller
Create app/javascript/controllers/reset_form_controller.js:
```javascript
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
reset() {
this.element.reset()
}
}
```
Update Room Show View
Edit app/views/rooms/show.html.erb and add data-controller:
erb
<div class="container mx-auto px-4 max-w-4xl"
data-controller="room"
data-room-room-id-value="<%= @room.id %>">
<!-- rest of the code -->
</div>
✅ Checkpoint: Messages broadcast in real-time via WebSocket
Task 8: Test Real-time Features
Start Services
# Terminal 1: Start Redis
redis-server
# Terminal 2: Start Rails
./bin/dev
Test in Multiple Browsers
- Open http://localhost:3000 in Chrome
- Sign in as alice
- Join "General" room
- Open http://localhost:3000 in Firefox (or Incognito)
- Sign in as bob
- Join "General" room
- Send messages from both browsers
- Messages should appear instantly in both windows
✅ Checkpoint: Real-time messaging works across browsers
Task 9: Add Auto-scroll
The auto-scroll is already implemented in the Stimulus controller from Task 7.
Test:
1. Send multiple messages to fill the chat window
2. New messages should automatically scroll to bottom
3. If manually scrolled up, new messages still append (no auto-scroll)
✅ Checkpoint: Auto-scroll working
Task 10: Mobile Responsiveness
Tailwind CSS handles most responsiveness. Test on mobile:
1. Open in Chrome DevTools mobile view
2. Test room list
3. Test chat interface
4. Ensure input is accessible above keyboard
Optional improvements in app/views/rooms/show.html.erb:
erb
<!-- Make chat area taller on mobile -->
<div id="messages" class="p-4 h-[60vh] md:h-96 overflow-y-auto space-y-3">
✅ Checkpoint: Works on mobile devices
Task 11: Error Handling
Add Connection Status Indicator
Update app/javascript/controllers/room_controller.js:
```javascript
_connected() {
console.log("Connected to room channel")
this.showStatus("Connected", "green")
}
_disconnected() {
console.log("Disconnected from room channel")
this.showStatus("Disconnected", "red")
}
showStatus(message, color) {
// Optional: Add status indicator to UI
}
```
Handle Failed Message Send
Already handled in messages_controller.rb with validation errors.
✅ Checkpoint: Basic error handling in place
Task 12: Final Testing
Testing Checklist
- [ ] Sign up with valid data
- [ ] Sign up with invalid data (see errors)
- [ ] Sign in with correct password
- [ ] Sign in with wrong password
- [ ] View room list
- [ ] Create new room
- [ ] Join existing room
- [ ] See message history
- [ ] Send message (appears for sender)
- [ ] Open second browser, send message (appears in first browser)
- [ ] Messages persist after refresh
- [ ] Sign out works
- [ ] Mobile responsive
- [ ] No console errors
Performance Check
- [ ] Page loads in < 2 seconds
- [ ] Messages appear in < 100ms
- [ ] No N+1 queries (check logs)
- [ ] Redis connected
✅ Checkpoint: All features working, MVP complete!
Task 13: Optional Enhancements
User Presence
Track online users in each room:
- Add Stimulus controller to track presence
- Broadcast join/leave events
- Display online user list
- Show join/leave notifications
Typing Indicators
Show "User is typing..." indicator:
- Debounce keystrokes
- Broadcast typing event
- Display indicator in UI
- Clear after 3 seconds
Message Timestamps
Better formatting:
# In message partial
<%= time_ago_in_words(message.created_at) %> ago
Deployment
Heroku Deployment
heroku create your-chat-app
heroku addons:create heroku-postgresql:mini
heroku addons:create heroku-redis:mini
git push heroku main
heroku run rails db:migrate
heroku run rails db:seed
heroku open
Environment Variables
heroku config:set ACTION_CABLE_ALLOWED_ORIGINS=https://your-app.herokuapp.com
Maintenance Tasks
Database Cleanup
# Remove old messages (optional cron job)
Message.where("created_at < ?", 30.days.ago).destroy_all
Monitor Connections
# Rails console
ActionCable.server.connections.count
Success Metrics
After completing all tasks:
- ✅ Users can register and authenticate
- ✅ Users can create and join rooms
- ✅ Messages broadcast in real-time
- ✅ WebSocket connection stable
- ✅ UI responsive on mobile
- ✅ No major bugs or errors
- ✅ Code organized and readable
- ✅ Ready for deployment
🎉 Congratulations! Your Rails chat app with Action Cable is complete!
Notes
Agent Notes
Implementation Tips and Gotchas
This document contains practical notes, tips, and common issues encountered during implementation.
Action Cable Gotchas
1. Redis Must Be Running
Problem: WebSocket connects but messages don't broadcast.
Symptom:
Connection successful but no messages appearing
Solution:
```bash
Make sure Redis is running
redis-server
Or in Docker
docker run -d -p 6379:6379 redis:7-alpine
```
Verify:
```bash
redis-cli ping
Should return: PONG
---
### 2. Cookie-based Authentication
**Problem:** WebSocket connection rejected.
**Symptom:**
An unauthorized connection attempt was rejected
```
Solution: Ensure session cookie is set after login:
```ruby
In SessionsController#create
session[:user_id] = user.id
```
Important: Action Cable uses the same session cookie as Rails, so authentication "just works" if session is properly set.
3. Turbo Streams vs Manual Broadcasting
Problem: Confused about when to use which.
Best Practice:
```ruby
✅ Use Turbo Streams (automatic)
class Message < ApplicationRecord
aftercreatecommit do
broadcastappendto(
"room#{roomid}",
target: "messages",
partial: "messages/message"
)
end
end
❌ Don't manually broadcast HTML
ActionCable.server.broadcast("room1", { html: rendermessage })
```
Why: Turbo Streams handle DOM updates automatically.
4. N+1 Queries in Real-time
Problem: Slow message rendering.
Bad:
ruby
def recent_messages
messages.order(created_at: :desc).limit(100)
end
Good:
ruby
def recent_messages
messages.includes(:user).order(created_at: :desc).limit(100)
end
Verify in logs:
```
Bad (N+1)
SELECT * FROM messages WHERE room_id = 1
SELECT * FROM users WHERE id = 1
SELECT * FROM users WHERE id = 2
...
Good (2 queries)
SELECT * FROM messages WHERE room_id = 1
SELECT * FROM users WHERE id IN (1, 2, 3, ...)
```
Stimulus Controller Tips
1. Auto-scroll on New Message
Implementation:
```javascript
_received(data) {
// Turbo Stream handles DOM update
// Just scroll after a short delay
setTimeout(() => this.scrollToBottom(), 50)
}
scrollToBottom() {
const container = document.getElementById("messages")
container.scrollTop = container.scrollHeight
}
```
Why the delay: Turbo Stream needs time to append DOM before scrolling.
2. Clear Input After Send
Problem: Input box doesn't clear after sending message.
Solution: Use Stimulus controller:
```javascript
// resetformcontroller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
reset(event) {
if (event.detail.success) {
this.element.reset()
}
}
}
```
In form:
erb
<%= form_with model: [@room, @message],
data: {
controller: "reset-form",
action: "turbo:submit-end->reset-form#reset"
} do |f| %>
3. Unsubscribe on Disconnect
Problem: Multiple subscriptions created when navigating.
Solution: Always unsubscribe:
javascript
disconnect() {
if (this.channel) {
this.channel.unsubscribe()
}
}
Database Tips
1. Composite Index for Messages
Why:
sql
-- Common query pattern
SELECT * FROM messages
WHERE room_id = 1
ORDER BY created_at DESC
LIMIT 100;
Migration:
ruby
add_index :messages, [:room_id, :created_at]
Result: 10x faster queries on large datasets.
2. Dependent Destroy
Important: Set up cascading deletes:
```ruby
class Room < ApplicationRecord
has_many :messages, dependent: :destroy
end
class User < ApplicationRecord
has_many :messages, dependent: :destroy
end
```
Why: Prevents orphaned records.
Tailwind CSS Tips
1. Scrollable Chat Container
<div class="h-96 overflow-y-auto">
<!-- messages -->
</div>
Mobile-friendly:
erb
<div class="h-[60vh] md:h-96 overflow-y-auto">
2. Message Bubble Layout
<div class="flex items-start space-x-3">
<div class="flex-1 bg-gray-100 rounded-lg p-3">
<!-- message content -->
</div>
</div>
Why flex-1: Allows message to expand but not overflow.
Testing Tips
1. Multi-browser Testing
# Regular browser
open http://localhost:3000
# Incognito/Private
# Chrome: Cmd+Shift+N (Mac) / Ctrl+Shift+N (Windows)
# Firefox: Cmd+Shift+P (Mac) / Ctrl+Shift+P (Windows)
Pro tip: Use different user agents to see different usernames.
2. WebSocket Debugging
Browser Console:
```javascript
// Check connection status
App.cable.connection.webSocket.readyState
// 0 = CONNECTING, 1 = OPEN, 2 = CLOSING, 3 = CLOSED
// See active subscriptions
App.cable.subscriptions.subscriptions
```
Rails Console:
```ruby
Check active connections
ActionCable.server.connections.count
Broadcast manually
ActionCable.server.broadcast("room_1", { test: "message" })
```
3. Redis Debugging
redis-cli
# List all keys
KEYS *
# Monitor in real-time
MONITOR
# Get value
GET action_cable:...
Performance Optimization
1. Limit Message History
Don't load all messages:
```ruby
Bad
@messages = @room.messages.includes(:user)
Good
@messages = @room.recent_messages(100)
```
2. Redis Connection Pool
For production:
```yaml
config/cable.yml
production:
adapter: redis
url: <%= ENV['REDISURL'] %>
channelprefix: chatappproduction
poolsize: <%= ENV.fetch("RAILSMAX_THREADS") { 5 } %>
```
3. Compress Large Payloads
For images/files (future feature):
```ruby
Use Active Storage with CDN
Don't broadcast binary data via Action Cable
---
## Security Notes
### 1. Sanitize User Input
**Already handled by Rails:**
```erb
<!-- Automatic HTML escaping -->
<p><%= message.content %></p>
For rich text (future):
```ruby
gem 'sanitize'
Sanitize before save
beforesave :sanitizecontent
def sanitize_content
self.content = Sanitize.fragment(content, Sanitize::Config::BASIC)
end
```
2. Rate Limiting (Optional)
Prevent spam:
```ruby
Gemfile
gem 'rack-attack'
config/initializers/rack_attack.rb
Rack::Attack.throttle("messages/ip", limit: 10, period: 60) do |req|
req.ip if req.path.start_with?('/rooms') && req.post?
end
```
3. Channel Authorization
Already implemented:
```ruby
app/channels/application_cable/connection.rb
def connect
self.currentuser = findverified_user
end
def findverifieduser
if verifieduser = User.findby(id: cookies.encrypted[:userid])
verifieduser
else
rejectunauthorizedconnection
end
end
```
Result: Only authenticated users can connect.
Deployment Notes
Heroku-specific
1. Redis Addon:
bash
heroku addons:create heroku-redis:mini
2. Action Cable Configuration:
```ruby
config/environments/production.rb
config.actioncable.url = ENV.fetch("ACTIONCABLEURL") {
"wss://#{ENV['HEROKUAPPNAME']}.herokuapp.com/cable"
}
config.actioncable.allowedrequestorigins = [
"https://#{ENV['HEROKUAPPNAME']}.herokuapp.com"
]
```
3. Environment Variables:
bash
heroku config:set ACTION_CABLE_URL=wss://your-app.herokuapp.com/cable
Database Performance
1. Connection Pool:
```yaml
config/database.yml
production:
pool: <%= ENV.fetch("RAILSMAXTHREADS") { 5 } %>
```
2. Query Timeout:
```ruby
config/database.yml
production:
variables:
statement_timeout: 5000 # 5 seconds
```
Common Error Messages
"Redis connection refused"
Cause: Redis not running
Fix:
bash
redis-server
"Couldn't find Room with 'id'="
Cause: Missing room parameter or invalid ID
Fix: Check routes and form submission
"An unauthorized connection attempt was rejected"
Cause: User not logged in
Fix: Ensure session[:user_id] is set
"ActionController::InvalidAuthenticityToken"
Cause: Missing CSRF token
Fix: Ensure <%= csrf_meta_tags %> in layout
Future Enhancements
1. Direct Messages
Schema:
```ruby
createtable :conversations do |t|
t.references :sender, foreignkey: { totable: :users }
t.references :receiver, foreignkey: { to_table: :users }
t.timestamps
end
createtable :directmessages do |t|
t.references :conversation
t.references :user
t.text :content
t.timestamps
end
```
2. Typing Indicators
Channel method:
ruby
def typing
broadcast_to(
room,
type: "typing",
user: current_user.username
)
end
Client debounce:
javascript
typing = debounce(() => {
this.channel.perform("typing")
}, 300)
3. Read Receipts
Schema:
ruby
add_column :messages, :read_by, :jsonb, default: []
Track:
ruby
def mark_as_read(user_id)
update(read_by: read_by + [user_id])
end
4. Message Editing
Schema:
ruby
add_column :messages, :edited_at, :datetime
Broadcast update:
ruby
after_update_commit do
broadcast_replace_to(
"room_#{room_id}",
target: self,
partial: "messages/message"
)
end
Best Practices Summary
Do's ✅
- Use Turbo Streams for DOM updates
- Eager load associations (
.includes(:user)) - Limit message history (100 messages max)
- Unsubscribe when leaving channel
- Test in multiple browsers
- Use Redis in production
- Set up proper indexes
Don'ts ❌
- Don't manually manipulate DOM in channel callbacks
- Don't forget to start Redis
- Don't broadcast large payloads
- Don't skip authentication checks
- Don't load all messages at once
- Don't forget CSRF tokens
- Don't ignore N+1 queries
Debugging Workflow
Check Rails logs:
bash
tail -f log/development.log
Check Redis:
bash
redis-cli ping
-
Check browser console:
- Look for WebSocket connection
- Check for JavaScript errors
-
Test in Rails console:
```rubyTest models
room = Room.first
room.messages.create!(user: User.first, content: "test")
# Test broadcasting
ActionCable.server.broadcast("room_1", { message: "test" })
```
-
Monitor Action Cable:
bash # Logs show connections and subscriptions Started GET "/cable" for 127.0.0.1 Successfully upgraded to WebSocket
Resources
Official Docs
Useful Gems
-
redis- Redis client -
bcrypt- Password hashing -
tailwindcss-rails- Styling -
turbo-rails- Hotwire Turbo -
stimulus-rails- Stimulus JS
Final Checklist
Before considering the project complete:
- [ ] Redis running and connected
- [ ] Users can sign up/in/out
- [ ] Rooms can be created
- [ ] Messages send in real-time
- [ ] Messages persist in database
- [ ] UI works on mobile
- [ ] No N+1 queries
- [ ] No console errors
- [ ] WebSocket reconnects automatically
- [ ] Code is clean and organized
- [ ] Ready for deployment
Support & Troubleshooting
If you encounter issues:
- Check this document first
- Review error messages carefully
- Test each component individually
- Use Rails console for debugging
- Check Redis and PostgreSQL are running
- Verify browser WebSocket support
Remember: Most Action Cable issues are caused by:
- Redis not running
- Missing authentication
- N+1 queries
- Subscription not unsubscribing
Good luck with your implementation! 🚀