Escalated for Rails
A full-featured, embeddable support ticket system for Rails. Drop it into any app — get a complete helpdesk with SLA tracking, escalation rules, agent workflows, and a customer portal. No external services required.
Three hosting modes. Run entirely self-hosted, sync to a central cloud for multi-app visibility, or proxy everything to the cloud. Switch modes with a single config change.
Features
- Ticket lifecycle — Create, assign, reply, resolve, close, reopen with configurable status transitions
- SLA engine — Per-priority response and resolution targets, business hours calculation, automatic breach detection
- Escalation rules — Condition-based rules that auto-escalate, reprioritize, reassign, or notify
- Agent dashboard — Ticket queue with filters, bulk actions, internal notes, canned responses
- Customer portal — Self-service ticket creation, replies, and status tracking
- Admin panel — Manage departments, SLA policies, escalation rules, tags, and view reports
- File attachments — Drag-and-drop uploads with configurable storage and size limits
- Activity timeline — Full audit log of every action on every ticket
- Email notifications — Configurable per-event notifications with webhook support
- Department routing — Organize agents into departments with auto-assignment (round-robin)
- Tagging system — Categorize tickets with colored tags
-
Inertia.js + Vue 3 UI — Shared frontend via
@escalated-dev/escalated
v0.4.0 — Advanced Features
- Bulk actions — Assign, change status/priority, add tags, close, or delete multiple tickets at once
- Macros — Reusable multi-step automations (set status + assign + add note in one click)
- Ticket followers — Agents follow tickets and receive the same notifications as the assignee
- Satisfaction ratings — 1-5 star CSAT ratings with optional comments after resolution
- Pinned notes — Pin important internal notes to the top of the ticket thread
- Keyboard shortcuts — Full keyboard navigation for power users
- Quick filters — One-click filter chips (My Tickets, Unassigned, Urgent, SLA Breaching)
- Presence indicators — See who else is viewing a ticket in real-time
- Enhanced dashboard — CSAT metrics, resolution times, SLA breach tracking
Requirements
- Ruby 3.1+
- Rails 7.1+
- Node.js 18+ (for frontend assets)
Quick Start
bundle add escalated
npm install @escalated-dev/escalated
rails generate escalated:install
rails db:migrateAdd the Ticketable concern to your User model:
class User < ApplicationRecord
include Escalated::Ticketable
endDefine authorization in your ApplicationController or an initializer:
# config/initializers/escalated.rb
Escalated.configure do |config|
config.admin_check = ->(user) { user.admin? }
config.agent_check = ->(user) { user.agent? || user.admin? }
endVisit /support — you're live.
Frontend Setup
Escalated uses Inertia.js with Vue 3. The frontend components are provided by the @escalated-dev/escalated npm package.
Tailwind Content
Add the Escalated package to your Tailwind content config so its classes aren't purged:
// tailwind.config.js
content: [
// ... your existing paths
'./node_modules/@escalated-dev/escalated/src/**/*.vue',
],Page Resolver
Add the Escalated pages to your Inertia page resolver:
// app/javascript/entrypoints/application.js
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
createInertiaApp({
resolve: name => {
if (name.startsWith('Escalated/')) {
const escalatedPages = import.meta.glob(
'../../../node_modules/@escalated-dev/escalated/src/pages/**/*.vue',
{ eager: true }
)
const pageName = name.replace('Escalated/', '')
return escalatedPages[`../../../node_modules/@escalated-dev/escalated/src/pages/${pageName}.vue`]
}
const pages = import.meta.glob('../pages/**/*.vue', { eager: true })
return pages[`../pages/${name}.vue`]
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el)
},
})Theming (Optional)
Register the EscalatedPlugin to render Escalated pages inside your app's layout — no page duplication needed:
import { EscalatedPlugin } from '@escalated-dev/escalated'
import AppLayout from '@/layouts/AppLayout.vue'
createInertiaApp({
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.use(EscalatedPlugin, {
layout: AppLayout,
theme: {
primary: '#3b82f6',
radius: '0.75rem',
}
})
.mount(el)
},
})Your layout component must accept a #header slot and a default slot. Escalated will render its sub-navigation in the header and page content in the default slot. Without the plugin, Escalated uses its own standalone layout.
See the @escalated-dev/escalated README for full theming documentation and CSS custom properties.
Hosting Modes
Self-Hosted (default)
Everything stays in your database. No external calls. Full autonomy.
Escalated.configure do |config|
config.mode = :self_hosted
endSynced
Local database + automatic sync to cloud.escalated.dev for unified inbox across multiple apps. If the cloud is unreachable, your app keeps working — events queue and retry.
Escalated.configure do |config|
config.mode = :synced
config.hosted_api_url = "https://cloud.escalated.dev/api/v1"
config.hosted_api_key = ENV["ESCALATED_API_KEY"]
endCloud
All ticket data proxied to the cloud API. Your app handles auth and renders UI, but storage lives in the cloud.
Escalated.configure do |config|
config.mode = :cloud
config.hosted_api_url = "https://cloud.escalated.dev/api/v1"
config.hosted_api_key = ENV["ESCALATED_API_KEY"]
endAll three modes share the same controllers, UI, and business logic. The driver pattern handles the rest.
Configuration
Create or edit config/initializers/escalated.rb:
Escalated.configure do |config|
config.mode = :self_hosted
config.user_class = "User"
config.table_prefix = "escalated_"
config.route_prefix = "support"
config.default_priority = :medium
# Middleware
config.middleware = [:authenticate_user!]
config.admin_middleware = nil
# Tickets
config.allow_customer_close = true
config.auto_close_resolved_after_days = 7
config.max_attachments_per_reply = 5
config.max_attachment_size_kb = 10240
# SLA
config.sla = {
enabled: true,
business_hours_only: true,
business_hours: {
start: 9, end: 17,
timezone: "UTC",
working_days: [1, 2, 3, 4, 5]
}
}
# Notifications
config.notification_channels = [:email]
config.webhook_url = nil
# Storage (ActiveStorage)
config.storage_service = :local
endScheduling
Add these to your scheduler for SLA and escalation automation:
# config/schedule.rb (whenever gem) or use solid_queue/sidekiq-cron
every 1.minute do
runner "Escalated::CheckSlaJob.perform_now"
end
every 5.minutes do
runner "Escalated::EvaluateEscalationsJob.perform_now"
end
every 1.day do
runner "Escalated::CloseResolvedJob.perform_now"
end
every 1.week do
runner "Escalated::PurgeActivitiesJob.perform_now"
endRoutes
Routes are automatically mounted when the engine loads. By default they mount at /support.
| Route | Method | Description |
|---|---|---|
/support |
GET | Customer ticket list |
/support/create |
GET | New ticket form |
/support/{ticket} |
GET | Ticket detail |
/support/agent |
GET | Agent dashboard |
/support/agent/tickets |
GET | Agent ticket queue |
/support/agent/tickets/{ticket} |
GET | Agent ticket view |
/support/admin/reports |
GET | Admin reports |
/support/admin/departments |
GET | Department management |
/support/admin/sla-policies |
GET | SLA policy management |
/support/admin/escalation-rules |
GET | Escalation rule management |
/support/admin/tags |
GET | Tag management |
/support/admin/canned-responses |
GET | Canned response management |
/support/agent/tickets/bulk |
POST | Bulk actions on multiple tickets |
/support/agent/tickets/{ticket}/follow |
POST | Follow/unfollow a ticket |
/support/agent/tickets/{ticket}/macro |
POST | Apply a macro to a ticket |
/support/agent/tickets/{ticket}/presence |
POST | Update presence on a ticket |
/support/agent/tickets/{ticket}/pin/{reply} |
POST | Pin/unpin an internal note |
/support/{ticket}/rate |
POST | Submit satisfaction rating |
Events
Connect to ticket lifecycle events via ActiveSupport::Notifications:
ActiveSupport::Notifications.subscribe("escalated.ticket_created") do |event|
ticket = event.payload[:ticket]
# Handle new ticket
endAlso Available For
- Escalated for Laravel — Laravel Composer package
- Escalated for Rails — Ruby on Rails engine (you are here)
- Escalated for Django — Django reusable app
- Escalated for AdonisJS — AdonisJS v6 package
- Escalated for Filament — Filament v3 admin panel plugin
- Shared Frontend — Vue 3 + Inertia.js UI components
Same architecture, same Vue UI, same three hosting modes — for every major backend framework.
Testing
bundle exec rspecLicense
MIT