Skip to content
Support System

Product

Product Case Study

Flagship narrative: problem, users, principles, the ticketing model, key decisions, trade-offs, and what is not built.

Source: support-system: README.md and the backend and frontend source

Support System: Product Case Study

This is a retrospective, written after the build, from the working system. It does not claim customers, users, validated demand, or measured results. Where this document describes a benefit, it is a design intent, not an outcome.


Summary

Support System is a multi-tenant helpdesk and contact-centre platform: a Go API and workers, a Next.js agent app, and a visual bot-flow builder. It handles tickets from six channels, routes them to agents, measures them against SLA targets, and reports on the result.

The case study is about the product rules more than the features. Ticketing looks simple until each rule has to be stated: what a status means, what a customer reply does in each state, when a clock stops, and who is allowed to receive a ticket. Those rules were written down, implemented, and checked against the code. They are collected in the lifecycle specification.

The problem

Support teams lose time and trust in the gaps between tools and between people.

  • Each channel (email, chat, WhatsApp, SMS, phone) tends to create its own queue.
  • Work is assigned by whoever notices it, so a returning customer often explains the problem again to someone new.
  • SLA targets are tracked after the fact, so a breach is a report instead of a warning.
  • Supervisors learn a queue is backing up from a complaint, not from a screen.
  • Automation is bolted on without an explanation, so nobody can say why a ticket went where it did.

Who it is for

UserWhat they need
Support agentWork that is routed to them fairly, the customer's history in view, no surprise reassignments
SupervisorA live view of queue and agents, SLA risk before a breach, control over routing rules
Workspace administratorRoles, channels, business hours, branding, and an audit trail
IntegratorA scoped, idempotent API and signed webhooks
CustomerA reply on the channel they used, and a follow-up that is not lost

The user definitions are reasoned, not researched. See the personas for what that means.

Product principles

  1. Every state means one thing. Six statuses, fifteen allowed transitions, no free-form state.
  2. Fairness is measured in business time. The SLA clock runs only while the team owes the customer a move.
  3. Automation explains itself. Every assignment stores a method and a reason.
  4. Rules are layered, not forked. Defaults, then workspace, then queue; each layer overrides only what it names.
  5. Say what is not built. Exclusions are part of the scope, not a footnote.

What was built

  • Ticketing: a three-pane inbox, replies and internal notes with mentions and attachments, macros, merge and link, bulk actions, saved views, a command palette, collision detection when two agents open one ticket, and desktop notifications.
  • Routing: queues with round-robin, least-loaded, or manual strategies; skills with proficiency; languages; capacity; shifts; presence with reason codes; overflow between queues; sticky routing; rebalancing; and release rules.
  • SLA: ordered policies chosen by condition, business-hour calendars with holidays, three clocks per ticket, pause and resume, warnings, breaches, and escalation through automations.
  • Channels: website chat widget, email (IMAP, SMTP, or a raw MIME webhook), WhatsApp Cloud API, SMS through Twilio, voice and CTI events, and a signed API channel.
  • Bot flows: a node editor with typed sockets and a test simulator, versioned publishing, and 27 registered node types, two of which are canvas annotations.
  • Integration: a REST API with an OpenAPI document covering part of the surface, scoped API keys, OAuth2 client credentials, idempotency keys, signed webhooks with retries and redelivery, and CRM and ERP connectors.
  • Operations: a live wallboard, analytics with CSV export, CSAT surveys, and a knowledge base with a public help centre.
  • Administration: roles built from a catalog of 47 permission keys, branding applied at runtime, and an append-only audit log.

Key product decisions

Closed tickets are final

A closed ticket cannot be changed, reassigned, merged, or replied to publicly. A customer who writes after closing gets a new ticket titled as a follow-up, linked to the old one.

  • Why: closing counts the ticket in resolution metrics. Reopening it would rewrite history.
  • Cost: the customer sees what feels like one conversation as two ticket numbers, so the link has to be easy to follow.

The SLA clock pauses on pending and on hold

Waiting on a customer or on a third party is not the team's delay. Resume moves every due date forward by the business time spent paused.

  • Why: a wall-clock SLA turns nights and weekends into breaches nobody caused and hides real slowness behind noise.
  • Cost: resume has to recompute against a calendar, and an agent can use pending to stop a clock. That needs a metric watching it. The KPI framework lists it as not yet instrumented.

Customer replies reopen pending and solved tickets, but not on-hold ones

This is the rule the code implements. A pending ticket is waiting on the customer, so a reply ends the wait. A solved ticket was wrong, so it reopens. An on-hold ticket waits on someone else, so the customer's message does not change who owes the next move.

  • Note: a comment in the code mentions only solved tickets. The behaviour for pending tickets is deliberate in effect, and it is documented here so it is a stated rule.

Sticky routing, with eligibility checks

A returning customer goes back to the agent who last replied to them, if that agent is active, online, in the queue's team, in shift, skilled, and fluent in the customer's language. Capacity is not checked by default.

  • Why: continuity shortens resolution and spares the customer a repeat explanation.
  • Cost: it can load one agent unevenly. A workspace can switch capacity checking on, and rebalancing protects sticky assignments by default.

A reason on every assignment

Method and reason are stored with the assignment: auto_least_loaded, auto_round_robin, sticky, reopen_sticky, rebalanced, or manual. A ticket that could not be assigned stores why: no online agent, no qualified agent, all at capacity, no agent in shift, or a manual queue.

  • Why: the question supervisors actually ask is why a ticket went where it did. This answers it without reading logs.

Layered routing policy

Defaults, then a workspace override, then a queue override. Overrides are partial, unknown keys are rejected, and values are range-checked on save.

  • Why: queues differ. A billing queue wants continuity, and a first-line queue wants speed.
  • Cost: behaviour depends on three places, so the settings page marks which values are customised.

Tenant isolation enforced twice

Every query filters by tenant, and PostgreSQL row-level security is forced on every tenant table.

  • Why: a missed filter in a multi-tenant system is a data breach. A database-enforced second layer turns that mistake into an empty result.
  • Cost: work that crosses tenants has to opt in explicitly.

No AI

The product question was whether the ticketing rules hold up. A language model would have given a demo and tested nothing. It is on the roadmap as a later item.

Trade-offs accepted

  • A broad feature surface in one solo project, with test coverage concentrated where the rules are: the state machine, agent selection, the routing policy, and the SLA calendar.
  • A custom router, dependency wiring, and outbox instead of frameworks, which keeps dependencies low and moves maintenance onto the project.
  • Local disk for attachments, which is enough to prove the product and unfit for anything hosted.
  • Full-text search for the knowledge base rather than semantic search.

What was verified, and what was not

Verified by inspection of the code: the status transitions, the reply and reopen behaviour, the SLA pause and resume logic, the routing decision order, the policy layering, the auto-close job, and the counts quoted in this case study.

Automated tests that exist: 35 backend test functions in six files covering the ticket state machine, SLA calendar, agent selection, routing policy and rebalancing, the bot engine, and router permission guards; one frontend unit test file; and four browser tests run against a seeded backend. An end-to-end smoke script exercises authentication, CSRF, roles, idempotency, webhooks, bot handoff, tenant isolation, and analytics against a running stack.

Not verified: behaviour under load; any real-world use; the security properties in isolation; the database-backed ticket flows as isolated tests. No continuous integration runs any of the suites.

Limitations

  • No users, no customer discovery, no measured outcomes.
  • Runs locally on seeded demo data. Not deployed.
  • Most backend modules have no unit tests.
  • No continuous integration and no coverage measurement.
  • No AI features, billing, customer portal, or single sign-on.
  • OpenAPI documentation covers part of the API.

What I would do next

See the roadmap. In order: run the existing tests in CI and add direct tests for tenant isolation, instrument the missing metrics, finish the API documentation, deploy a staging environment, and only then consider a customer portal, single sign-on, and AI-assisted triage.

What I learned

  • The hard part of a ticketing system is the rules between the states, not the states.
  • A policy a supervisor cannot read will not be changed. Writing each rule in plain language was as much work as the rule.
  • A reason stored on every automated decision is cheap to design in and expensive to add later.
  • A fair SLA is a trade-off. It protects agents and hands them a lever, so the metric that watches the lever belongs in the same document.
  • Requirements written after the build are documentation, not analysis. They were still the fastest way to find where the code and the intent disagreed.