Open-source live chat for Rails. Alternative to Crisp, Intercom and Chatwoot — inside your Rails app, as a gem, not another service to deploy.
Your users already have questions. livechat gives them a chat bubble and
gives you an inbox, inside the app you already run. Visitors write — signed
in or not. Your team answers — together, every reply signed with its author.
Nobody is around? Visitors leave an email and the conversation continues
there. Every word stays in your database.
- A gem, not a platform.
bundle add livechat, one migration, one line in your layout. No second app to deploy, no third-party script on your pages, no per-seat pricing. Ever. - Zero UI dependencies. The widget is plain JavaScript and styles itself. No Tailwind, no Stimulus, no importmap, no build step, no websockets to configure. Works with Turbo Drive and strict nonce-based CSPs out of the box.
- Honest about response time. The widget says "We usually reply within a few hours" (you choose the words), not a fake "we're online". Email notifications — both directions — keep slow conversations alive.
- A team sport. Any authorized teammate answers any thread; each message carries its author. You decide what visitors see — full names, first names, or an anonymous "Support team".
- Real attribution. Signed-in visitors are identified server-side against your user records and keep their thread across devices. Guests get a cookie, and keep their history when they sign up.
- Send files, both ways. Visitors and agents attach images and documents; images show inline. Files are served through the engine — gated exactly like the chat — never a public blob URL. Needs Active Storage; degrades to text-only where it's absent.
- 26 languages. The widget follows your app's locale, RTL included.
- Add
<%= livechat_tag %>to your layout. A chat bubble appears bottom-right (or open the panel from any element withdata-livechat-open, orwindow.Livechat.open()). - A visitor writes. The message lands in
livechat_conversationsin your database, and — if you configured it — in your team's email. - Your team answers at the mount path (
/livechat). Several people can work the same thread; resolve it when done. A visitor writing again reopens it — one thread per visitor, like a conversation, not tickets. - The visitor sees the reply in the widget, or by email when they're gone.
Realtime is polling, on purpose: ~4s while the panel is open, ~30s in the
background, nothing at all for visitors who never wrote. No Action Cable,
no Redis, no infrastructure. At support-chat volume you will not notice;
your ops person will notice there is nothing new to run. If you already run
Action Cable and want replies to land the instant they're sent, turn it on
(config.action_cable = true) — polling stays the fallback.
- Ruby >= 3.2
- Rails >= 7.1
# Gemfile
gem "livechat"bundle install
bin/rails generate livechat:install
bin/rails db:migrateThe generator writes config/initializers/livechat.rb, creates the
migration, and mounts the engine at /livechat. Then add the widget to your
layout:
<%= livechat_tag %>That's it. Visit any page, click the bubble, say hi. Answer yourself at
/livechat.
Everything lives in config/initializers/livechat.rb; every option has a
working default. The essentials:
Livechat.configure do |config|
config.current_user = ->(request) { request.env["warden"]&.user }
config.authorize_agent = ->(request) { request.env["warden"]&.user&.admin? }
config.mailer_from = "chat@example.com"
config.agent_emails = -> { User.where(admin: true).pluck(:email) }
config.reply_time_text = "We usually reply within an hour."
endcurrent_user (and any admin gate) receives the raw request, so it works
with whatever auth you have:
# Devise / Warden:
config.current_user = ->(request) { request.env["warden"]&.user }
# Rails 8 built-in auth (bin/rails generate authentication):
config.current_user = lambda do |request|
token = request.cookies["session_token"]
Session.find_signed(token)&.user if token
endconfig.accent_color = "#7c3aed"One hex value restyles the launcher, header, visitor bubbles and send button; the widget picks black or white text automatically for contrast, in light and dark mode alike.
Replies are signed. What visitors see is up to you:
config.agent_display_name = ->(label) { label.split.first } # "Ada"
config.agent_display_name = ->(_label) { "Support team" } # anonymousWhen a visitor writes and nobody has read it, the team gets one email — one
per unread stretch, not one per message. When an agent replies and the
visitor is away, the visitor gets one email (signed-in visitors
automatically, guests once they leave an address — the widget asks, gently).
Requires config.mailer_from; team notifications also need
config.agent_emails.
For anything else, hook in:
config.on_visitor_message = ->(message) { SlackNotifier.ping(message) }On by default wherever the app has Active Storage. If you don't already:
bin/rails active_storage:install && bin/rails db:migrateVisitors get a paperclip in the composer; agents get a file field on the reply form. Images render inline in the thread, other files as download links. Every file is served through the engine's own route and gated the same way the chat is — an agent, or the visitor who owns that conversation — so nothing leaks through a guessable or long-lived blob URL.
config.attach_files = true # false turns it off even with Active Storage
config.max_attachments = 5 # per message
config.max_attachment_size = 10.megabytes
config.allowed_attachment_types = nil # or an allowlist, e.g. %w[image/png image/jpeg application/pdf]Where Active Storage isn't installed, the widget quietly stays text-only.
Polling is the default and needs nothing from your app. If you already run Action Cable, turn on push so a reply appears the instant it's sent:
config.action_cable = true
config.action_cable_url = "/cable" # match your `mount ActionCable... => ...`A new message nudges the widget and the inbox to refresh at once; polling
stays the fallback, so a dropped socket or a proxy that blocks WebSockets
never means a missed message. The widget speaks the Action Cable protocol
over a plain WebSocket — no @rails/actioncable, no build step — and only
ever subscribes to a stream the server signed for it. Under a strict CSP,
allow the socket with connect-src 'self'.
Browse at the mount path: open and resolved tabs, unread badges, search
(visitor name, email, and everything anyone wrote), and a column showing
which teammates have worked each thread. One click into a thread; reply
(Cmd/Ctrl+Enter sends), resolve, reopen. Both pages keep themselves fresh
while you watch — and never reload over a half-written reply or search.
Gated by config.authorize_agent (development-only until you set it).
window.Livechat.open()/window.Livechat.close()—open("Hi, I need help with…")prefills the message box (never over a visitor's draft)- Any element with
data-livechat-openopens the panel on click; adddata-livechat-message="…"to prefill — great for contextual buttons ("Request verification", "Ask about billing") - While replies are unread, every
data-livechat-openelement carries a small count badge — so hiding the launcher doesn't hide the answer <%= livechat_button %>renders a plain, unstyled opener buttonconfig.show_launcher = falsehides the bubble entirely — bring your own entry point
No AI bots, no canned responses, no omnichannel (WhatsApp, Messenger…), no visitor tracking, no "powered by" badge. If you need a support platform, Chatwoot is excellent. If you need your users to be able to reach you from inside your Rails app — this is a gem's worth of exactly that.
bundle exec rake test
bundle exec rubocopMIT.