Project

escalated

0.0
No release in over 3 years
A full-featured support ticket system with SLA, escalation rules, and three hosting modes.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

>= 2.0
>= 7.0
 Project Readme

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:migrate

Add the Ticketable concern to your User model:

class User < ApplicationRecord
  include Escalated::Ticketable
end

Define 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? }
end

Visit /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
end

Synced

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"]
end

Cloud

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"]
end

All 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
end

Scheduling

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"
end

Routes

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
end

Also Available For

Same architecture, same Vue UI, same three hosting modes — for every major backend framework.

Testing

bundle exec rspec

License

MIT