Skip to content

v0.1.0

Choose a tag to compare

@rameerez rameerez released this 16 Sep 04:57
· 68 commits to main since this release

Customer support for any Rails app: tickets that are real conversations.

Somebody asks for help about something in your app. A desk answers. Your staff sign their replies. Your team works a queue. All of it on threads your users already understand, because it is the same messaging they use for everything else.

support_desk is a product gem on the chats kernel: chats owns the transcript, realtime, attachments, read state and moderation; this gem owns everything a case needs on top. The same shape as usage_credits on wallets.

class User < ApplicationRecord
  acts_as_messager                      # chats
  has_support_tickets                   # the person asking
  acts_as_support_agent if: :admin?     # the person answering
end

class Order < ApplicationRecord
  supportable topic: :order             # "I need help with this order"
end

ticket = alice.ask_support!("It never arrived", about: order)
ticket.assign!(to: lucia)
ticket.reply!("Looking into it now", by: lucia)
ticket.close!(by: lucia)

What ships in 0.1.0, and what each is for

Cases that are conversations

  • A ticket is one chats conversation, with the ticket as its subject. One transcript, one read horizon, one realtime story, one moderation contract.
  • Why: assignment, response clocks and audit are all per case. A single eternal thread per customer cannot answer "who owns this, and how long have they been waiting".

A desk that is not a person

  • The desk sends every reply; humans and bots author them. Your users see one counterpart, "Support", with a signature under each answer.
  • Why: staff come and go and hand cases to each other. Handoffs change who writes, never who the thread is with, and nobody's personal profile is exposed to customers.

An inbox that stays usable

  • Every case with the desk collapses into one row in the user's messages, with an aggregate unread count. Tapping it opens a support-only list.
  • Why: a customer with a dozen past cases should not have their real conversations pushed off the screen. This is a chats primitive, so it also works for shops, organizations or bots.

Topics, as a tree you define in code

  • topic :order, about: Order with pickers, prefill, visibility rules, per-topic routing and priority, and a required free-form exit.
  • Why: a topic decides which of the user's records they can attach, what the wizard asks, and where the case lands. That is behaviour, so it belongs in code, reviewed and versioned.

Assignment as a history

  • Take, assign, hand off with a note, release, plus the full record of who held a case, when, and why they stopped.
  • Why: "time per agent", shift handovers and drop-in cover are unanswerable from a single assignee column.

A console you bring your own UI to

  • Three layers, stop at whichever you like: query objects and presenters that work in any UI; a controller concern plus a routing concern for any admin framework; or rails g support_desk:console madmin, which writes a full console into your app that you own and can edit.
  • A mountable turnkey console ships too, for apps with no admin framework.

Requester screens, included

  • A mounted engine with the list, a three-step wizard, entry points you drop anywhere with link_to_support about: @order, and ejectable views.
  • Hotwire Native path rules included, so the flow behaves on mobile.

Events, not opinions

  • SupportDesk.on(:ticket_opened), :requester_replied, :ticket_transitioned and nine more. Multi-subscriber, error-isolated, emitted after commit.
  • Why: the gem never sends your email, push or Slack. It tells you what happened and stays out of the way. A subscriber that raises is reported and never rolls back a ticket.

Things it refuses to get wrong

  • Authorization both ways. A requester cannot close, refile, reassign or reopen someone else's case, and cannot reach a topic hidden from them by typed path, deep link or a record's own topic. An agent cannot read a desk they are not allowed to see, and the refusal does not leak whether a case exists.
  • Concurrency. "One open case per thing" is enforced by a partial unique index on PostgreSQL and SQLite, so two simultaneous requests produce one case, not two. MySQL has no partial indexes, so there the model plus SupportDesk.doctor hold the line, and the migration says so.
  • Privacy. Internal notes are events, never messages: they never reach the customer's thread and never appear in a data export. Notification titles carry no customer name by default.
  • Idempotency. A redelivered message event never rewinds a clock or reopens a case twice.
  • Replayable state. SupportDesk.doctor checks the invariants the database cannot, and exits non-zero, so a mis-wired desk fails your build rather than a customer's support screen.

Verified

Tests 638 runs, 2904 assertions, 0 failures, 0 skips
Databases SQLite, PostgreSQL
Rails 7.2, 8.0, 8.1
Lint / security rubocop clean, brakeman 0 warnings

Built against the released chats 0.2.0, not a local checkout.

Full detail in CHANGELOG.md and the README.

Full Changelog: https://github.com/rameerez/support_desk/commits/v0.1.0