TurboChat
TurboChat is a mountable Rails chat engine for server-rendered apps.
What you get:
- Turbo Stream chat UI.
- Role-based permissions.
- Mentions, typing signals, invites, moderation.
- Practical customization hooks without rewriting everything.
What this is not:
- A hosted chat service.
- A React-first component library.
- A "just add JS" widget.
Use This If
Use TurboChat if your app is Rails-first and you want chat now, not after building a custom permission/event/moderation system.
Skip it if you want fully custom frontend architecture from day one.
Install
# Gemfile
gem "turbo_chat"bundle install
bin/rails generate turbo_chat:install
bin/rails db:migrateMount with an explicit helper prefix:
# config/routes.rb
mount TurboChat::Engine => "/chat", as: "turbo_chat"Opinionated recommendation: mount under /chat (or another scoped path), not root.
Participant Resolution (Most Important Section)
This is where installs usually fail. TurboChat now resolves the current participant in this order:
-
current_chat_participanton your hostApplicationController(preferred explicit hook). -
config.current_participant_resolver(custom resolver lambda). -
current_user(if your app exposes it).
If none are available, TurboChat raises NotImplementedError with guidance.
Return value must be:
-
nil(unauthenticated), or - a model that uses
acts_as_chat_participant.
If you use Devise and your User model is a chat participant, this works out of the box.
Host Contract
Participant model opt-in
class User < ApplicationRecord
acts_as_chat_participant
endOptional explicit hook (recommended for clarity)
class ApplicationController < ActionController::Base
def current_chat_participant
current_user
end
endOptional custom resolver (for non-current_user auth)
TurboChat.configure do |config|
config.current_participant_resolver = ->(controller) { controller.send(:current_member) }
endFirst Working Usage
chat = TurboChat::Chat.create!(title: "Support")
TurboChat::ChatMembership.create!(chat: chat, participant: current_user, role: :admin)<%= link_to "Open chat", turbo_chat.chat_path(chat) %>Routes You Actually Need
From the mounted engine:
GET /chatsGET /chats/:idPOST /chatsPATCH /chats/:id/acceptPATCH /chats/:id/declinePATCH /chats/:id/leavePATCH /chats/:id/closePATCH /chats/:id/reopenPOST /chats/:chat_id/chat_membershipsPOST /chats/:chat_id/chat_messagesPATCH /chats/:chat_id/chat_messages/:id
Recommended Baseline Config
Keep this simple initially:
TurboChat.configure do |config|
config.permission_adapter = TurboChat::Permission
config.max_chat_participants = 10
config.max_message_length = 1000
config.message_history_limit = 200
config.active_chat_window = 5.minutes
config.enable_mentions = true
config.mention_filter_exclude_self = true
config.mention_filter_hide_roles = true
config.enable_emoji_aliases = true
config.blocked_words = []
config.blocked_words_action = :reject # or :scramble
config.render_message_html = false
config.show_timestamp = true
config.show_role = false
config.emit_typing_events = false
config.emit_message_events = false
config.emit_mention_events = false
config.emit_invitation_events = false
config.emit_chat_lifecycle_events = false
config.emit_moderation_events = false
config.emit_blocked_words_events = false
endOpinionated defaults:
- Leave HTML rendering off unless required.
- Leave event emission off unless consumed.
- Keep permissions adapter default until you have a concrete policy gap.
Roles and Permissions
Built-in roles:
membermoderatoradmin
Behavior summary:
-
member: view/post and member mentions. -
moderator: invite,@all, role mentions, mute/timeout/ban, delete lower-rank messages. -
admin: moderator powers plus close/reopen chat.
Hard rules:
- Moderation is rank-based.
- No self-moderation.
- Closed chat blocks posting.
- Muted/timed-out members cannot post.
Custom role example:
TurboChat.configure do |config|
config.add_role(
:support_agent,
name: "Support Agent",
rank: 1,
permissions: %i[view_chat post_message delete_message]
)
endInvitations
- Invite creation:
POST /chats/:chat_id/chat_memberships - Accept:
PATCH /chats/:id/accept - Decline:
PATCH /chats/:id/decline
Important constraints:
- Invite type must match inviter participant base class.
- Participant limits apply to active memberships.
- Re-invite reactivates existing memberships.
Messages, Mentions, Signals
Messages
- Realtime append/update/remove via Turbo Streams.
- Inline edit for your own messages (permission-gated).
- History capped by
message_history_limit.
Mentions
-
@username,@all,@ROLE. - Server-side mention permission validation.
- Autocomplete defaults: excludes self, hides roles.
Signals
Automatic typing loop is built in.
Manual APIs:
TurboChat::Signals.start!(chat: chat, participant: current_user, signal_type: :thinking)
TurboChat::Signals.replace!(chat: chat, participant: current_user, signal_type: :planning)
TurboChat::Signals.clear!(chat: chat, participant: current_user)TurboChat::Signals.with(chat: chat, participant: current_user, signal_type: :thinking) do
# work
endModeration API
TurboChat::Moderation.mute_member!(actor: moderator, membership: membership)
TurboChat::Moderation.unmute_member!(actor: moderator, membership: membership)
TurboChat::Moderation.timeout_member!(actor: moderator, membership: membership, until_time: 30.minutes.from_now)
TurboChat::Moderation.clear_timeout!(actor: moderator, membership: membership)
TurboChat::Moderation.ban_member!(actor: moderator, membership: membership)
TurboChat::Moderation.delete_message!(actor: moderator, message: message)
TurboChat::Moderation.close_chat!(actor: admin, chat: chat)
TurboChat::Moderation.reopen_chat!(actor: admin, chat: chat)Raises:
TurboChat::Moderation::AuthorizationErrorTurboChat::Moderation::InvalidActionError
Browser Events
Enable with config flags.
Event names:
turbo-chat:typing-startedturbo-chat:typing-endedturbo-chat:message-sentturbo-chat:mentionturbo-chat:invitation-acceptedturbo-chat:chat-invitedturbo-chat:chat-joinedturbo-chat:chat-declinedturbo-chat:chat-leftturbo-chat:chat-closedturbo-chat:chat-reopened
document.addEventListener("turbo-chat:message-sent", function (event) {
console.log(event.detail.chatId);
});Client namespace: window.TurboChatUI
ActiveSupport::Notifications
Moderation (emit_moderation_events = true):
turbo_chat.moderation.member_mutedturbo_chat.moderation.member_unmutedturbo_chat.moderation.member_timed_outturbo_chat.moderation.member_timeout_clearedturbo_chat.moderation.member_bannedturbo_chat.moderation.message_deletedturbo_chat.moderation.chat_closedturbo_chat.moderation.chat_reopened
Blocked words (emit_blocked_words_events = true):
turbo_chat.blocked_words.detectedturbo_chat.blocked_words.rejectedturbo_chat.blocked_words.scrambled
UI Customization
Do this in order:
- Theme with config (
own_message_hex_color,role_message_hex_colors,mention_mark_hex_color, formatters). - Add class-level customization via
message_css_class_resolver. - Only then override markup partials.
Primary partial override point:
app/views/turbo_chat/chat_messages/_message.html.erb
Keep id="<%= dom_id(chat_message) %>" on the message wrapper or Turbo replacements break.
Internal Names
Use TurboChat in app code.
These are implementation identifiers used by the engine:
- Engine namespace in source:
TurboChat - Database table prefix:
turbo_chat_ - Browser event prefix:
turbo-chat:* -
ActiveSupport::Notificationsprefix:turbo_chat.*
Mount with as: "turbo_chat" and use turbo_chat.* route helpers.
Upgrade
bin/rails turbo_chat:install:migrations
bin/rails db:migrateDependencies
- Ruby
>= 3.1 - Rails
>= 7.0,< 8.0 -
turbo-rails>= 1.4,< 3.0 - PostgreSQL or SQLite
Development
bundle exec rake test