21 KiB
Chat API Surface Documentation
Purpose: Document all chat-related API endpoints, WebSocket messages, database models, and data structures.
Date: 2026-01-26 Phase: 06-session-chat-research-design Plan: 01
Overview
The chat system uses a hybrid architecture:
- REST API for sending messages and fetching history
- WebSocket (Protocol Buffers) for real-time message delivery
- No jamClient methods - chat is purely web-based (no native client integration)
Supported Channels:
global- All online users (disabled for school users)session- Users in a specific music sessionlesson- Direct messages between teacher and student in a lesson
REST API Endpoints
Base Path
All endpoints are under /api/chat (configured in web/config/routes.rb)
Authentication
All endpoints require:
before_filter :api_signed_in_user- valid session/tokenbefore_filter :check_session- validates session/lesson access
1. Create Chat Message
Endpoint: POST /api/chat
Controller: ApiChatsController#create
File: /web/app/controllers/api_chats_controller.rb
Request Body:
{
"message": "Hello world",
"channel": "session",
"music_session": "session-uuid",
"lesson_session": "lesson-uuid",
"client_id": "client-uuid",
"target_user": "user-uuid"
}
Parameters:
message(string, required) - Message text (1-255 chars, profanity filtered)channel(string, required) - Channel type:"global","session","lesson"music_session(string, conditional) - Required ifchannel="session"lesson_session(string, conditional) - Required ifchannel="lesson"client_id(string, optional) - Client UUID for routing (excludes sender from broadcast)target_user(string, conditional) - Required ifchannel="lesson"(recipient user ID)
Response (Success 200):
{
"id": "msg-uuid",
"message": "Hello world",
"user_id": "user-uuid",
"session_id": null,
"created_at": "2026-01-26T12:00:00.000Z",
"channel": "session",
"lesson_session_id": null,
"purpose": null,
"user": {
"name": "John Doe"
},
"music_notation": null,
"claimed_recording": null
}
Response (Error 404):
{
"error": "specified session not found"
}
Response (Error 403):
{
"error": "not allowed to join the specified session"
}
Response (Error 422):
{
"errors": {
"message": ["is too short (minimum is 1 character)"],
"user": ["Global chat is disabled for school users"]
}
}
Business Logic (from ChatMessage.create):
- Validates message length (1-255 chars) and profanity
- HTML sanitizes message content (strict mode)
- For lesson channel:
- Auto-determines source_user and target_user from lesson
- Sets unread flags:
teacher_unread_messagesorstudent_unread_messages - Sends email notification if target user offline
- Saves message to database
- Broadcasts via WebSocket (see WebSocket section)
Access Control:
- Global channel: Blocked for school users (
user.school_idpresent) - Session channel: Must be participant in session (
music_session.access?(user)) - Lesson channel: Must be teacher or student in lesson (
lesson_session.access?(user)) - School protection: Can only chat with users from same school (unless platform instructor)
2. Get Chat History
Endpoint: GET /api/chat
Controller: ApiChatsController#index
File: /web/app/controllers/api_chats_controller.rb
Query Parameters:
GET /api/chat?channel=session&music_session=session-uuid&limit=20&page=0
Parameters:
channel(string, required) - Channel type:"global","session","lesson"music_session(string, conditional) - Required ifchannel="session"lesson_session(string, conditional) - Required ifchannel="lesson"limit(integer, optional) - Number of messages to fetch (default: 20)page(integer, optional) - Page number (default: 0)start(integer, optional) - Offset cursor for pagination
Response (Success 200):
{
"next": 20,
"chats": [
{
"id": "msg-uuid",
"message": "Hello world",
"user_id": "user-uuid",
"session_id": null,
"created_at": "2026-01-26T12:00:00.000Z",
"channel": "session",
"lesson_session_id": null,
"purpose": null,
"user": {
"name": "John Doe"
},
"music_notation": null,
"claimed_recording": null
}
]
}
Response Fields:
next(integer|null) - Cursor for next page, ornullif no more messageschats(array) - Array of chat messages (newest first, due todefault_scopeorder)
Business Logic (from ChatMessage.index):
- Queries by channel
- If
music_sessionparam: filters bymusic_session_id - If
lesson_sessionparam: filters bylesson_session_id - Orders by
created_at DESC(newest first) - Includes associated
userrecords (eager loading) - Returns pagination cursor if more messages available
Access Control:
- Same as create endpoint (validates session/lesson access)
- Global channel: Returns empty array for school users
WebSocket Messages
Protocol
- Uses Protocol Buffers (defined in
pb/src/client_container.proto) - Routed through
websocket-gatewayservice - Published via RabbitMQ to appropriate targets
Message Type: CHAT_MESSAGE
Protocol Buffer Definition:
message ChatMessage {
optional string sender_name = 1;
optional string sender_id = 2;
optional string msg = 3;
optional string msg_id = 4;
optional string created_at = 5;
optional string channel = 6;
optional string lesson_session_id = 7;
optional string purpose = 8;
optional string attachment_id = 9;
optional string attachment_type = 10;
optional string attachment_name = 11;
}
JSON Equivalent (for jam-ui WebSocket handling):
{
"sender_name": "John Doe",
"sender_id": "user-uuid",
"msg": "Hello world",
"msg_id": "msg-uuid",
"created_at": "2026-01-26T12:00:00.000Z",
"channel": "session",
"lesson_session_id": null,
"purpose": null,
"attachment_id": null,
"attachment_type": null,
"attachment_name": null
}
Field Descriptions:
sender_name- Display name of sendersender_id- User UUID of sendermsg- Message text contentmsg_id- Unique message ID (for deduplication)created_at- ISO 8601 timestampchannel- Channel type:"global","session","lesson"lesson_session_id- Lesson UUID (if lesson channel)purpose- Optional purpose:"Notation File","Audio File","JamKazam Recording","Video Uploaded", etc.attachment_id- ID of attached resource (notation, recording, video)attachment_type- Type:"notation","audio","recording","video"attachment_name- Display name of attachment
Message Routing (from ChatMessage.send_chat_msg)
Session Channel:
@@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id})
- Broadcasts to all users in the session
- Excludes sender (by client_id)
- Route target:
session:{session_id}
Global Channel:
@@mq_router.publish_to_active_clients(msg)
- Broadcasts to all active (online) clients
- Route target:
__ALL_ACTIVE_CLIENTS__
Lobby Channel:
@@mq_router.publish_to_active_clients(msg)
- Same as global (TODO: should filter to lobby users only)
- Route target:
__ALL_ACTIVE_CLIENTS__
Lesson Channel:
@@mq_router.publish_to_user(target_user.id, msg, sender = {:client_id => client_id})
@@mq_router.publish_to_user(user.id, msg, sender = {:client_id => client_id})
- Sends to both teacher and student
- Route target:
user:{user_id}
Client-Side WebSocket Handling
Registration (legacy pattern):
context.JK.JamServer.registerMessageCallback(
context.JK.MessageType.CHAT_MESSAGE,
function (header, payload) {
// Handle incoming chat message
ChatActions.msgReceived(payload);
}
);
jam-ui Integration:
- Uses
JamServer.jshelper for WebSocket management - Messages routed through Redux middleware (similar to MIXER_CHANGES, JAM_TRACK_CHANGES)
- Should subscribe/unsubscribe based on session state
Database Model
Table: chat_messages
Schema File: /ruby/db/schema.rb
Table Definition:
CREATE TABLE chat_messages (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) REFERENCES users(id),
music_session_id VARCHAR(64) REFERENCES music_sessions(id),
message TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
channel VARCHAR(128) NOT NULL DEFAULT 'session',
target_user_id VARCHAR(64) REFERENCES users(id),
lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id) ON DELETE CASCADE,
purpose VARCHAR(200),
music_notation_id VARCHAR(64) REFERENCES music_notations(id),
claimed_recording_id VARCHAR(64) REFERENCES claimed_recordings(id)
);
Indexes:
CREATE INDEX chat_messages_idx_channels ON chat_messages(channel);
CREATE INDEX chat_messages_idx_created_at ON chat_messages(created_at);
CREATE INDEX chat_messages_idx_music_session_id ON chat_messages(music_session_id);
Columns:
id- UUID primary keyuser_id- Sender user ID (FK to users)music_session_id- Session ID (FK to music_sessions, nullable)message- Message text (TEXT, NOT NULL)created_at- Timestamp (default: current_timestamp)channel- Channel type (default: "session")target_user_id- Recipient user ID for direct messages (FK to users, nullable)lesson_session_id- Lesson ID (FK to lesson_sessions, nullable, CASCADE delete)purpose- Optional purpose/type (VARCHAR 200)music_notation_id- Attached notation file (FK to music_notations, nullable)claimed_recording_id- Attached recording (FK to claimed_recordings, nullable)
ActiveRecord Model
File: /ruby/lib/jam_ruby/models/chat_message.rb
Class: JamRuby::ChatMessage
Associations:
belongs_to :user
belongs_to :music_session
belongs_to :target_user, class_name: "JamRuby::User"
belongs_to :lesson_session, class_name: "JamRuby::LessonSession"
belongs_to :music_notation, class_name: "JamRuby::MusicNotation"
belongs_to :claimed_recording, class_name: "JamRuby::ClaimedRecording"
Validations:
validates :user, presence: true
validates :message, length: {minimum: 1, maximum: 255}, no_profanity: true
validate :same_school_protection
validate :no_global_for_schools
Default Scope:
default_scope { order('created_at DESC') }
Note: Messages are ordered newest-first by default!
HTML Sanitization:
html_sanitize strict: [:message]
Strips all HTML tags from message content.
Constants
Channels:
CHANNEL_SESSION = 'session'
CHANNEL_LESSON = 'lesson'
CHANNEL_LOBBY = 'lobby'
Note: global channel is used but not defined as constant.
Related Models
lesson_sessions Table
Unread Message Tracking (for lesson chat):
ALTER TABLE lesson_sessions ADD COLUMN teacher_unread_messages BOOLEAN DEFAULT FALSE NOT NULL;
ALTER TABLE lesson_sessions ADD COLUMN student_unread_messages BOOLEAN DEFAULT FALSE NOT NULL;
Business Logic:
- When student sends message: sets
teacher_unread_messages = true - When teacher sends message: sets
student_unread_messages = true - Flags should be cleared when user opens lesson chat (not implemented in code yet)
Email Notifications:
- If target user is offline (
!target.online?) and message is present - Sends email via
UserMailer.lesson_chat(chat_msg).deliver_now
Data Flow Diagrams
Create Message Flow
Client (jam-ui)
↓ POST /api/chat
ApiChatsController#create
↓
ChatMessage.create(user, session, message, channel, client_id, target_user, lesson)
↓ Validates + Sanitizes
↓ Saves to DB
ChatMessage.send_chat_msg(...)
↓
MessageFactory.chat_message(...) → Protocol Buffer
↓
MQRouter.publish_to_*
↓ RabbitMQ
WebSocket Gateway
↓ WebSocket
All Clients (in channel)
↓
JamServer.handleMessage(CHAT_MESSAGE)
↓
ChatActions.msgReceived(payload)
↓
Redux Store Update
↓
UI Re-render
Get History Flow
Client (jam-ui)
↓ GET /api/chat?channel=session&music_session=123
ApiChatsController#index
↓
ChatMessage.index(user, params)
↓ Queries DB (with pagination)
↓ Includes user records
↓ Returns {chats: [...], next: cursor}
Rabl Template (api_chats/index.rabl)
↓ JSON serialization
Client receives chat history
↓
Merges with Redux store
↓
UI displays messages
Example API Calls
Create Session Message
Request:
POST /api/chat
Content-Type: application/json
Cookie: remember_token=...
{
"message": "Great jam everyone!",
"channel": "session",
"music_session": "abc123",
"client_id": "client-uuid-456"
}
Response:
{
"id": "msg-789",
"message": "Great jam everyone!",
"user_id": "user-123",
"session_id": null,
"created_at": "2026-01-26T15:30:00.000Z",
"channel": "session",
"lesson_session_id": null,
"purpose": null,
"user": {
"name": "John Doe"
},
"music_notation": null,
"claimed_recording": null
}
WebSocket Broadcast (to all session participants):
{
"type": "CHAT_MESSAGE",
"chat_message": {
"sender_name": "John Doe",
"sender_id": "user-123",
"msg": "Great jam everyone!",
"msg_id": "msg-789",
"created_at": "2026-01-26T15:30:00.000Z",
"channel": "session",
"lesson_session_id": null,
"purpose": null
}
}
Get Session History
Request:
GET /api/chat?channel=session&music_session=abc123&limit=20&page=0
Cookie: remember_token=...
Response:
{
"next": 20,
"chats": [
{
"id": "msg-789",
"message": "Great jam everyone!",
"user_id": "user-123",
"session_id": null,
"created_at": "2026-01-26T15:30:00.000Z",
"channel": "session",
"lesson_session_id": null,
"purpose": null,
"user": {
"name": "John Doe"
},
"music_notation": null,
"claimed_recording": null
},
{
"id": "msg-788",
"message": "Let's start!",
"user_id": "user-456",
"session_id": null,
"created_at": "2026-01-26T15:29:00.000Z",
"channel": "session",
"lesson_session_id": null,
"purpose": null,
"user": {
"name": "Jane Smith"
},
"music_notation": null,
"claimed_recording": null
}
]
}
Pagination:
- If
nextis not null: More messages available - Next request:
GET /api/chat?...&start=20
Create Global Message
Request:
POST /api/chat
Content-Type: application/json
Cookie: remember_token=...
{
"message": "Anyone want to jam?",
"channel": "global",
"client_id": "client-uuid-456"
}
Response:
{
"id": "msg-999",
"message": "Anyone want to jam?",
"user_id": "user-123",
"session_id": null,
"created_at": "2026-01-26T15:31:00.000Z",
"channel": "global",
"lesson_session_id": null,
"purpose": null,
"user": {
"name": "John Doe"
},
"music_notation": null,
"claimed_recording": null
}
WebSocket Broadcast (to all active clients):
{
"type": "CHAT_MESSAGE",
"chat_message": {
"sender_name": "John Doe",
"sender_id": "user-123",
"msg": "Anyone want to jam?",
"msg_id": "msg-999",
"created_at": "2026-01-26T15:31:00.000Z",
"channel": "global",
"lesson_session_id": null,
"purpose": null
}
}
Missing Functionality
Read/Unread Tracking
Current State:
- ✅ Exists for lesson chat:
teacher_unread_messages,student_unread_messagesboolean flags - ❌ Does NOT exist for session chat or global chat
- ❌ No per-message read receipts
- ❌ No persistent unread counts
What Needs to Be Added (Phase 6):
-
Database Schema Changes:
- Add
last_read_attimestamp to track when user last viewed channel - OR: Add
unread_countcache column per user/channel - OR: Add
message_readsjoin table for per-message tracking (overkill for MVP)
- Add
-
API Endpoints:
PUT /api/chat/mark_read- Mark messages as read for current user/channelGET /api/chat/unread_count- Get unread count per channel
-
WebSocket Messages:
- Broadcast read receipts to other participants (optional, Phase 11)
-
Business Logic:
- Increment unread count when message received (if user not viewing chat)
- Reset unread count when user opens chat window
- Show badge on chat button with unread count
Implementation Strategy (to be designed in Phase 6 Plan 2):
- Use localStorage for client-side unread tracking (simple, no schema changes)
- OR: Use server-side tracking with new
chat_message_readstable (more robust) - Preference: Server-side for cross-device consistency
File Attachments
Current State:
- ✅ Lesson chat supports attachments (
music_notation,claimed_recording) - ❌ Session chat does NOT support attachments
- ❌ No UI in jam-ui for attaching files
Out of Scope for Phase 6:
- File attachment functionality deferred to next milestone
- Can be added later without breaking changes (fields already exist)
jamClient Methods
Conclusion: Chat is purely web-based. No native client (C++) methods exist.
Relevant jamClient Calls:
jamClient.UserAttention(true)- Triggers system notification when message received (if chat hidden)
Note: In jam-ui, we won't use legacy jamClient directly. Instead, use JamServer.js for WebSocket and browser Notification API for desktop notifications.
Security & Validation
Message Validation
- Length: 1-255 characters
- Profanity filter:
no_profanity: truevalidation - HTML sanitization: Strips all tags (
html_sanitize strict: [:message])
Access Control
- Global Channel:
- Blocked for school users (
user.school_idpresent) - All non-school users can send/receive
- Blocked for school users (
- Session Channel:
- Must be participant:
music_session.access?(current_user) - Validated via
check_sessionbefore_filter
- Must be participant:
- Lesson Channel:
- Must be teacher or student:
lesson_session.access?(current_user) - Auto-routes messages between teacher/student
- School protection: Can only chat with same school (unless platform instructor)
- Must be teacher or student:
Rate Limiting
- NOT IMPLEMENTED in current API
- Consider adding in Phase 11 (error handling)
Performance Considerations
Database Indexes
- ✅ Indexed:
channel,created_at,music_session_id - ❌ NOT indexed:
lesson_session_id(should add if lesson chat heavily used) - ❌ NOT indexed:
user_id(consider adding for user history queries)
Query Patterns
- Default scope orders by
created_at DESC- always scans newest messages first - Pagination uses offset (not cursor) - can be slow for deep pages
- Eager loads
userrecords - prevents N+1 queries
Message Retention
- Global channel: Client-side limit of 100 messages (
max_global_msgs) - Session/lesson: No limit in database (messages persist forever)
- Consider: Add TTL or archiving for old messages (future optimization)
Integration Checklist for jam-ui
Phase 7 (Data Layer):
- Create Redux slice for chat messages (
chatSlice.js) - Add API client methods:
fetchChatHistory(),sendChatMessage() - Add WebSocket message handler for
CHAT_MESSAGEtype - Implement message deduplication (by
msg_id) - Add read/unread tracking logic
Phase 8 (UI Components):
- Create
JKSessionChatWindowcomponent - Render message list with timestamps
- Add message composition area (textarea + send button)
- Implement auto-scroll on new messages
- Add loading states (fetching history)
Phase 9 (State Management):
- Wire up Redux actions (send, receive, fetch history)
- Integrate WebSocket subscription
- Handle session join/leave (activate channel)
- Sync unread counts with Redux state
Phase 10 (Testing):
- Unit tests: API client, Redux reducers
- Integration tests: Message send/receive flow
- E2E tests: Complete chat workflow
Phase 11 (Error Handling):
- Handle API errors (404, 403, 422)
- Handle WebSocket disconnection
- Retry failed message sends
- Show error messages to user
Summary
REST API:
- ✅ Simple 2-endpoint design (create, index)
- ✅ Channel-based routing (global, session, lesson)
- ✅ Access control enforced
- ❌ No read/unread tracking for session chat
- ❌ No rate limiting
WebSocket:
- ✅ Real-time delivery via Protocol Buffers
- ✅ Efficient routing (session, user, broadcast)
- ✅ Attachment metadata included
Database:
- ✅ Single table for all channels
- ✅ Foreign keys for associations
- ✅ Indexes for common queries
- ❌ No read tracking columns
Missing Features:
- Read/unread tracking (session/global)
- Rate limiting
- Message deletion/editing
- File attachments for session chat
Next Steps:
- Analyze React patterns in jam-ui (CHAT_REACT_PATTERNS.md)
- Design Redux state structure (Phase 6 Plan 2)
- Design read/unread tracking strategy (Phase 6 Plan 2)