Skip to main content

Support System

The support system provides in-app ticket management for end users, a dedicated support agent portal, and super admin oversight. It covers ticket creation, messaging (with internal notes), FAQ management, agent assignment, dashboard analytics, and real-time WebSocket subscriptions.

Access Tiers

A user becomes a support agent when a super admin calls adminAssignSupportRole(userId). The SupportAgentGuard checks this at runtime. The User type exposes an isSupportAgent boolean field for the frontend to conditionally render the support portal.

info
Canonical support role UUID

SupportRoleService.onModuleInit (src/support/services/support-role.service.ts) upserts the global support role on every backend boot. The create branch pins the canonical UUID 1fd9c33f-52ec-4fc5-a9e9-102dcb4c6862 so any fresh database (new env, restored backup) lands on the same id deterministically. The update: {} clause on the upsert is intentional — pre-existing rows are not touched, so envs whose Role.id already drifted (for example, dev historically had 52007617-e2d6-45a8-83ec-5934d961a631) keep their current id. The support role flows through the UserRole global bridge, not UserCompanyRole, so it never participates in CompanyWriteGuard or per-company tier checks. Aligning a drifted env to the canonical id requires a manual SQL update against the Role table after first re-pointing any UserRole.roleId rows that reference the old id; only super admins can perform this against prod RDS.

Data Model

Ticket Lifecycle

Roles on a Ticket

A ticket can reference up to three distinct users:

FieldRequiredSet ByPurpose
createdByIdYescreateSupportTicket (current user)The reporter who filed the ticket
assignedToIdNoassignTicket (agent)The support agent handling triage and reply
testerIdNocreateSupportTicket (optional) or assignTester (agent)The user responsible for verifying the reported bug is fixed or the requested feature behaves correctly

The tester is intentionally separate from the assignee so that the person who works on the fix is not the same person who signs it off. Any user (not just support agents) can be set as the tester — the typical case is the original reporter, a developer, or a QA engineer. Setting testerId to null via assignTester clears the field. Tester changes are logged with action: TESTER_CHANGED.

After-Fix Status Transition ("Ready for Retest")

When a support ticket is fixed, deployed to prod, and handed to QA for verification, the status transition is always to TEST — never WAITING_ON_CUSTOMER, never RESOLVED. The UI column labeled "Ready for Retest" on the agent board is backed by backend status TEST; RESOLVED means the agent has signed off after retest passed.

mutation {
updateTicketStatus(ticketId: "<id>", status: "TEST") { id status }
assignTester(ticketId: "<id>", testerId: "<tester-user-id>") { id }
}

Both calls always go together: moving to TEST without a testerId leaves the ticket orphaned in the "Ready for Retest" column with no assignee to notify, and the TESTER_CHANGED event doesn't fire so no in-app/email notification is sent. See Email Notifications → Tester Assigned below for the notification payload.

WAITING_ON_CUSTOMER is now visible in the agent queue

Both halves of the old "replies vanish" problem were fixed on 2026-04-23:

  • Backend: sendSupportReply no longer auto-transitions the ticket to WAITING_ON_CUSTOMER — agents must set that status explicitly via updateTicketStatus when they genuinely need the reporter to respond.
  • Frontend: components/support/portal/TicketQueue.tsx removed WAITING_ON_CUSTOMER from DEFAULT_HIDDEN_STATUSES; a one-time localStorage migration (support-ticket-hidden-statuses-migration-v1) strips it from legacy persisted lists so existing agents see waiting tickets without toggling visibility. Only CLOSED is hidden by default now.

Reserve WAITING_ON_CUSTOMER for the case where we genuinely need input from the reporter (clarification, repro steps) — not for our own fix/retest cycle.

Historical incident (2026-04-21): Shipped a 21-ticket batch end-to-end and moved 16 of them to WAITING_ON_CUSTOMER instead of TEST. The UI's "Ready for Retest" column showed 12 tickets instead of the ~28 expected. Fix was a bulk updateTicketStatus(status: "TEST") + assignTester over the affected IDs. Always target TEST for post-deploy handoff.

Auto-Transitions

TriggerFromTo
Agent assigned to OPEN ticketOPENIN_PROGRESS
User replies while waitingWAITING_ON_CUSTOMERIN_PROGRESS
User replies to resolved ticketRESOLVEDOPEN
Cron job (hourly)RESOLVED > 7 daysCLOSED

Agent replies (sendSupportReply) do not auto-transition the status — that was removed on 2026-04-23 because the portal hid WAITING_ON_CUSTOMER and replied tickets dropped out of the replier's own queue.

Key Timestamps

FieldSet When
firstResponseAtFirst agent reply (never overwritten)
resolvedAtStatus changes to RESOLVED
closedAtStatus changes to CLOSED
lastActivityAtEvery status change, message, or assignment

Enums

TicketCategory

ValueDescription
GENERALGeneral inquiry
BILLINGBilling and payment
TECHNICALTechnical issue
ACCOUNTAccount management
FEATURE_REQUESTFeature request
BUG_REPORTBug report

TicketPriority

LOW | MEDIUM (default) | HIGH | URGENT

TicketStatus

Dynamic — not a Prisma enum. SupportTicket.status is a plain string whose allowed values come from the TicketStatusConfig table and are administered via adminCreateTicketStatus / adminUpdateTicketStatus / adminDeleteTicketStatus mutations.

Default seed (applied on startup if the table is empty):

namelabelcolorsortOrderflags
OPENOpen#3B82F60isDefault, isActive
IN_PROGRESSIn Progress#F59E0B1isInProgress, isActive
WAITING_ON_CUSTOMERWaiting on Customer#6B72802isWaiting, isActive
TESTTest#ae00ff3isInProgress, isActive
RESOLVEDResolved#10B9815isResolved, isActive
CLOSEDClosed#EF44446isTerminal, isActive

The name column is the machine identifier stored on tickets; the label column is the UI display string. Renaming via adminUpdateTicketStatus changes the label without touching ticket rows. The frontend reads the table at runtime (see Frontend Guide → Dynamic status rendering) — no hardcoded map, so an admin rename propagates without a redeploy.

warning
On 2026-04-17 the TEST row was renamed from the legacy default READY_FOR_RETEST. If you're looking at older dev data or an old frontend bundle that still hardcodes READY_FOR_RETEST, those tickets will render as "unknown status" — the fix is to drop the hardcoded labels (done in PR #644) and let the UI read from TicketStatusConfig.

TicketMessageSenderRole

USER | SUPPORT | SYSTEM

FaqStatus

DRAFT | PUBLISHED | ARCHIVED

TicketAuditAction

CREATED | STATUS_CHANGED | PRIORITY_CHANGED | CATEGORY_CHANGED | ASSIGNED | UNASSIGNED | REASSIGNED | TESTER_CHANGED | MESSAGE_SENT | INTERNAL_NOTE_ADDED | ATTACHMENT_ADDED | ATTACHMENT_REMOVED | DESCRIPTION_CHANGED | MESSAGE_EDITED | MESSAGE_DELETED | RESOLVED | CLOSED | REOPENED | DELETED | ARCHIVED | UNARCHIVED

Constants & Defaults

ConstantValue
Ticket number formatSUP-00001 (5-digit zero-padded)
Default page size (tickets)12
Default page size (audit log)30
Default page size (FAQs)12
Auto-close after RESOLVED7 days (hourly cron)
Presigned URL TTL15 minutes
Max attachments per message5
Max attachment size10 MB

File Attachments

Messages on tickets support file attachments using the platform's presigned URL upload pattern. Files are stored in S3 and download URLs are generated on-demand with 15-minute expiry.

Constraints

RuleValue
Max files per message5
Max file size10 MB
Content or attachmentsAt least one required per message
Allowed typesImages, PDFs, Office docs, text, CSV, ZIP

Attachments are supported on sendTicketMessage, sendSupportReply, and addInternalNote. The content field is optional when attachments are provided.

When creating a ticket with files, the frontend creates the ticket first, then sends an initial message with the attachments.

Removing Attachments

Once a message has been sent, individual attachments can still be removed via the removeTicketAttachment(attachmentId) mutation. This addresses the common case where a user accidentally uploads the wrong file and only notices after the message has been delivered.

RuleValue
Who can removeTicket creator, support agent, or super admin (server-side check)
Status restrictionNone — allowed even on CLOSED tickets
Time windowNone — no expiry on the removal window
Empty-message cascadeIf removing the last attachment leaves the message with no text, the parent TicketMessage is deleted in the same transaction (messageDeleted: true)
S3 cleanupDB-first inside the transaction; StorageService.deleteFile runs best-effort afterwards. Failures are logged but never rolled back — orphan blobs are recoverable, while a missing blob behind a live row is a permanently broken download link
AuditA TicketAuditLog row with action: ATTACHMENT_REMOVED and metadata: { attachmentId, messageId, filename, messageDeleted }
Real-timePublishes supportTicketEvent (NEW_MESSAGE) to agents and myTicketUpdated (NEW_MESSAGE) to the ticket creator so live ticket-detail subscribers refetch

Real-Time Subscriptions

Two WebSocket subscriptions power the real-time updates:

SubscriptionGuardChannelPurpose
myTicketUpdated(ticketId?)JwtAuthmyTicketUpdated:{userId}Notify ticket creator of replies, status changes
supportTicketEventJwtAuth + SupportAgentsupportTicketEventNotify all agents of new tickets, assignments, status changes

Event Types

TicketEventTypeTrigger
NEW_TICKETUser creates a ticket
NEW_MESSAGEAgent replies or user sends message
STATUS_CHANGEDAny status transition
ASSIGNEDTicket assigned or reassigned
TESTER_CHANGEDTester set, changed, or cleared via assignTester
PRIORITY_CHANGEDPriority updated
DESCRIPTION_CHANGEDTicket description edited

Email Notifications

Support events trigger branded in-app + email notifications via SupportNotificationListener. Emails go through generateEmailTemplate for consistent branding and are queued on the Bull mail queue for reliable delivery. All recipient queries filter isActive: true so deactivated accounts never receive notifications. Every outbound URL is built by buildAbsoluteUrl()getBaseUrl() (reads SERVER_LIVE_PROTOCOL + SERVER_LIVE_DOMAIN) so in-app links and email links always point at the same host.

Recipient matrix at a glance

EventIn-appEmailNotes
Ticket createdcreatorcreatorConfirmation receipt only — no fan-out to super admins
Support reply (agent replies)creatorcreator
Ticket assignednew assignee + previous (on reassign)new assignee onlyPrevious assignee gets in-app, not email
Tester changednew testernew testerEmail skipped when tester == assignee or no email on file
Status changedcreator + assignee (−actor)creator only (−actor)Assignees rely on the portal dashboard for status updates
Priority → URGENTcreator + assignee (−actor)creator + assignee (−actor)Fires only on non-URGENT → URGENT transitions
User messagesassignee, else silentsameNo fan-out to super admins when the ticket is unassigned
Category changedcreator + assignee (−actor)same
Description editedcreator + assignee (−actor)sameContent NOT included in the notification body
Internal noteassignee + tester (−author)sameCreator is never notified — internal notes are agent-only

Every notification type is gated by the user's notifyOnSupportTickets preference. Listener handlers are fire-and-forget (try/catch wrapped, never rethrow) so a queueing failure for one recipient never breaks the mutation or the other recipients.

Ticket Created

When a user creates a support ticket, the ticket creator receives a creation confirmation:

ChannelContent
In-appTitle Ticket received: SUP-XXXXX, link to /support/{id} (user-facing page)
EmailBranded "We've got your ticket SUP-XXXXX" with ticket number, subject, category, priority badge, and a "View Ticket" button

Historical context: this event previously fanned out to every super admin, which was too noisy in practice. The fan-out was removed — agents now discover new tickets via the supportTicketEvent real-time subscription (the portal dashboard), and the first agent is notified via email only when they get assigned (see below). The TICKET_CREATED event + SUPPORT_TICKET_CREATED enum are kept so a future triage-routing or analytics listener can wire a different handler without re-introducing the event.

Priority Badge Colors

PriorityBackgroundText
URGENT#dc3545 (red)White
HIGH#fd7e14 (orange)White
MEDIUM#ffc107 (yellow)Dark
LOW#28a745 (green)Dark

Support Reply

When a support agent replies to a ticket, the ticket creator receives:

ChannelContent
In-appPlain-text reply snippet (HTML stripped, truncated to 100 chars), link to /support/{id}
EmailBranded template with agent name, ticket subject, and rendered reply HTML (sanitized against the allow-list, truncated to 500 visible characters)

Ticket Assigned

When a super admin or agent calls assignTicket(ticketId, agentId), the newly-assigned agent receives:

ChannelContent
In-appTitle Ticket assigned to you: SUP-XXXXX, type SUPPORT_TICKET_ASSIGNED, priority URGENT if the ticket itself is URGENT else NORMAL
EmailBranded template including priority + category badges, "Assigned by" actor name, link to the support-portal detail page

On reassignment (previous assignee was non-null and differs from the new one), the previous assignee also receives an in-app notification (SUPPORT_TICKET_REASSIGNED) — no email. Self-assignment (actor == new assignee) and assigning to someone who already is the assignee are both no-ops.

Tester Assigned

When assignTester(ticketId, testerId) is called, the newly-assigned tester receives:

ChannelContent
In-appTitle "You have been set as tester on SUP-XXXXX", type SUPPORT_TICKET_TESTER_ASSIGNED, link to the ticket detail page
EmailBranded template: subject You have been set as tester on SUP-XXXXX — &lt;ticket subject&gt;, with category/priority badges and a "View Ticket" button

Dedupe rules — the handler skips the email (but still creates the in-app notification) when:

  • The tester has no email on file (newTesterEmail is null).
  • The tester is also the current assignee (assignedToId === newTesterId). The assignee email from assignTicket already covered this user.
  • The tester is the actor who triggered the change (self-assignment).

Clearing the tester (assignTester(ticketId, testerId: null)) emits the event but the listener returns early because newTesterId is null — no notification, no email.

Status Changed

When status transitions (via updateTicketStatus, bulkUpdateTicketStatus, closeMyTicket, reopenMyTicket, or the hourly auto-close cron), the listener notifies the ticket creator + assignee, deduped, excluding whoever made the change:

ChannelContent
In-appTicket status updated: SUP-XXXXX, message SUP-XXXXX moved from {oldLabel} to {newLabel}. URL is /support/{id} for the creator, /support-portal/tickets/{id} for agents so neither audience 404s.
EmailCreator only (and only if creator is not the actor). Assignees see status changes in the portal dashboard instead of their inbox.

The hourly auto-close cron emits a TICKET_STATUS_CHANGED per closed ticket with actorName = "System" and actorId = &lt;system super admin&gt;, so the email the creator receives reads "moved from RESOLVED to CLOSED by System".

Priority Escalated (→ URGENT)

updateTicketPriority emits TICKET_PRIORITY_URGENT only when the new priority is URGENT and the old priority was something else. Routine LOW↔MEDIUM↔HIGH re-triage stays silent so stakeholders aren't pinged every time a ticket is sorted.

ChannelContent
In-appTitle SUP-XXXXX escalated to URGENT, priority URGENT, type SUPPORT_TICKET_PRIORITY_ESCALATED
Email🚨 SUP-XXXXX escalated to URGENT — &lt;subject&gt; — red-accent template showing old → new priority and the actor who escalated

Recipients: creator + assignee, deduped, minus the actor.

User Message (user → agent)

When a ticket user sends a message via sendTicketMessage, the event only notifies the current assignee:

ChannelContent
In-appTitle New message from {senderName} on SUP-XXXXX, message SUP-XXXXX — {subject}: "{200-char preview}"
EmailNew message on SUP-XXXXX — &lt;subject&gt; — branded template with sender name and a sanitized content preview

If the ticket has no assignee, the handler stays silent — agents discover unassigned tickets through the supportTicketEvent PubSub stream in the support portal, and we explicitly chose not to fan out to super admins on user messages (same rule as TICKET_CREATED).

Category Changed

When an agent calls updateTicketCategory(ticketId, category) and the value actually changes (no-op reposts are skipped), creator + assignee (minus actor) receive:

ChannelContent
In-appCategory changed on SUP-XXXXX, message SUP-XXXXX — {oldLabel} → {newLabel}, type SUPPORT_TICKET_CATEGORY_CHANGED
EmailCategory updated on SUP-XXXXX — &lt;subject&gt; — shows old → new category and the actor name

Description Edited

updateTicketDescription (agent-side) and updateMyTicketDescription (user-side) both emit TICKET_DESCRIPTION_CHANGED. No-op reposts are skipped. Creator + assignee (minus actor) receive:

ChannelContent
In-appDescription edited on SUP-XXXXX, message {actorName} edited the description of SUP-XXXXX, type SUPPORT_TICKET_DESCRIPTION_CHANGED
EmailDescription edited on SUP-XXXXX — &lt;subject&gt; — "open the ticket to see the updated content"
Why the description content isn't included

The author may have been editing to redact something. Including the new content in the notification would leak what they were trying to clean up, so the notification only nudges the reader to open the ticket.

Internal Note

addInternalNote(ticketId, content) writes a ticket message with isInternalNote: true and fires TICKET_INTERNAL_NOTE. The handler notifies the assignee + tester, deduped, minus the author:

ChannelContent
In-appInternal note on SUP-XXXXX by {authorName}, message includes a 200-char plain-text preview, type SUPPORT_TICKET_INTERNAL_NOTE
EmailInternal note on SUP-XXXXX — &lt;subject&gt; — amber-accent template marking the note as agent-only, with a sanitized preview
Creator is never notified

Internal notes are agent-to-agent by design. The ticket creator must never receive one — excluding them is an explicit branch in the handler, not an accident of the recipient math.

Bulk operations

bulkAssignTickets and bulkUpdateTicketStatus emit the same per-ticket events as their single-item counterparts inside the loop — TICKET_ASSIGNED per assignment (plus TICKET_STATUS_CHANGED when the auto-transition from the default status fires), TICKET_STATUS_CHANGED per status move. actorName is resolved once per batch so we don't do N user lookups for a 50-ticket bulk.

Previously bulk mutations updated the DB + audit log but emitted nothing, so agents bulk-assigned to tickets and creators of bulk-closed tickets were never notified. That's fixed: per-item emits reuse the existing handlers' dedup rules so they're safe at any batch size.

Event Payloads

Every payload carries an "emit-time snapshot" — the listener trusts the payload fields verbatim and never re-queries the DB for the same row, which keeps the handler deterministic even if another agent mutates the ticket between emit and consume.

EventPayload fields
TICKET_CREATEDticketId, ticketNumber, subject, description, category, priority, createdById, createdByName, createdByEmail
TICKET_SUPPORT_REPLYticketId, ticketNumber, subject, createdById, replyContent, repliedById, repliedByName
TICKET_ASSIGNEDticketId, ticketNumber, subject, priority, category, newAssignedToId, newAssigneeEmail, newAssigneeFirstName, previousAssignedToId, isReassignment, ticketCreatorId, actorId, actorName
TICKET_TESTER_CHANGEDticketId, ticketNumber, subject, priority, category, newTesterId, newTesterEmail, newTesterFirstName, previousTesterId, ticketCreatorId, assignedToId, actorId, actorName
TICKET_STATUS_CHANGEDticketId, ticketNumber, subject, priority, category, oldStatus, newStatus, ticketCreatorId, creatorEmail, creatorFirstName, assignedToId, actorId, actorName
TICKET_PRIORITY_URGENTticketId, ticketNumber, subject, category, oldPriority, newPriority, ticketCreatorId, creatorEmail, creatorFirstName, assignedToId, assigneeEmail, assigneeFirstName, actorId, actorName
TICKET_MESSAGE_SENTticketId, ticketNumber, subject, priority, category, senderId, senderName, contentPreview, assignedToId
TICKET_CATEGORY_CHANGEDticketId, ticketNumber, subject, priority, oldCategory, newCategory, ticketCreatorId, creatorEmail, creatorFirstName, assignedToId, assigneeEmail, assigneeFirstName, actorId, actorName
TICKET_DESCRIPTION_CHANGEDticketId, ticketNumber, subject, priority, category, ticketCreatorId, creatorEmail, creatorFirstName, assignedToId, assigneeEmail, assigneeFirstName, actorId, actorName
TICKET_INTERNAL_NOTEticketId, ticketNumber, subject, priority, authorId, authorName, contentPreview, assignedToId, assigneeEmail, assigneeFirstName, testerId, testerEmail, testerFirstName

NotificationType enum

The Prisma NotificationType enum carries one value per wired handler. All are mapped to the notifyOnSupportTickets preference in notification-preference.service.ts so users can opt out of the whole category with a single toggle:

SUPPORT_TICKET_CREATED, SUPPORT_TICKET_REPLY, SUPPORT_TICKET_ASSIGNED, SUPPORT_TICKET_REASSIGNED, SUPPORT_TICKET_STATUS_CHANGED, SUPPORT_TICKET_TESTER_ASSIGNED, SUPPORT_TICKET_PRIORITY_ESCALATED, SUPPORT_TICKET_USER_MESSAGE, SUPPORT_TICKET_CATEGORY_CHANGED, SUPPORT_TICKET_DESCRIPTION_CHANGED, SUPPORT_TICKET_INTERNAL_NOTE.

Security

User-provided content is sanitized in two different ways depending on whether the field is plain text or rich-text HTML:

FieldSanitizerWhy
ticketNumber, subject, createdByName, createdByEmail, repliedByNamevalidator.escape()Plain text — escape <, >, &, ", ' so any HTML metachars render as literal text
description, replyContent (email body)sanitizeRichTextForEmail()Rich text from the ticket form / TipTap — runs through sanitize-html against an allow-list of safe formatting tags so paragraphs, bold, lists, and links render but <script>, <img>, event handlers, etc. are stripped
replyContent (in-app snippet)htmlToPlainText()The bell preview does not render HTML, so all tags are stripped before truncating to 100 chars

The sanitizeRichTextForEmail() allow-list is: p, br, strong, b, em, i, u, s, ul, ol, li, blockquote, code, pre, h1h4, a, span. Anchor tags get target="_blank" rel="noopener noreferrer" injected automatically; only http, https, and mailto URL schemes are allowed.

Truncation is done by visible character count (not raw HTML length) so half-open tags are never emitted. When content exceeds the 500-char limit, the sanitizer falls back to a plain-text + ellipsis paragraph.

Both helpers live in src/common/mail/sanitize-rich-text.ts and are reusable for any future email listener that needs to embed user-supplied rich text.

Implementation Files

FilePurpose
src/notification-system/listeners/support-notification.listener.tsEvent handlers, branded email composition
src/common/mail/sanitize-rich-text.tssanitizeRichTextForEmail() and htmlToPlainText() helpers (shared, reusable)
src/support/resolvers/user-support.resolver.tsEmits TICKET_CREATED with enriched payload
src/support/resolvers/support-portal.resolver.tsEmits TICKET_SUPPORT_REPLY with enriched payload

Cron Job

The TicketAutoCloseJob runs every hour at minute 0. It finds all tickets in RESOLVED status where resolvedAt is older than 7 days, then for each:

  1. Updates status to CLOSED, sets closedAt
  2. Writes an audit log entry with reason: auto_close_7_days
  3. Appends a system message: "This ticket was automatically closed after 7 days of inactivity"
  4. Emits a per-ticket TICKET_STATUS_CHANGED event with actorName = "System" so SupportNotificationListener.handleTicketStatusChanged delivers the standard status-change notification to creator + assignee — the creator gets the email ("moved from RESOLVED to CLOSED by System"), the assignee gets the in-app notification.
  5. Publishes a single batched supportTicketEvent to the agent dashboard so it refreshes once per cron sweep, not once per ticket.

The legacy TICKET_AUTO_CLOSED domain event was removed — it was emitted but never handled, so auto-closes used to go out silently. Per-ticket TICKET_STATUS_CHANGED gives everyone involved a real notification with zero duplication.

Backend Implementation Files

FilePurpose
src/support/support.module.tsModule definition
src/support/services/support-ticket.service.tsTicket CRUD, assignment, stats, auto-close
src/support/services/ticket-message.service.tsUser messages, agent replies, internal notes
src/support/services/ticket-audit.service.tsAudit log creation
src/support/services/faq.service.tsFAQ CRUD, voting, suggestion generation
src/support/services/support-role.service.tsRole management (auto-upserts on startup)
src/support/resolvers/user-support.resolver.tsUser-facing queries and mutations
src/support/resolvers/support-portal.resolver.tsAgent portal queries and mutations
src/support/resolvers/admin-support.resolver.tsSuper admin queries and mutations
src/support/resolvers/faq.resolver.tsFAQ queries and mutations
src/support/resolvers/ticket-attachment.resolver.tsdownloadUrl field resolver (S3 presigned)
src/support/resolvers/support-subscription.resolver.tsWebSocket subscriptions
src/support/resolvers/user-support-agent-field.resolver.tsUser.isSupportAgent field resolver
src/support/guards/support-agent.guard.tsSupportAgentGuard (checks role or superAdmin)
src/support/jobs/ticket-auto-close.job.tsHourly auto-close cron
src/notification-system/listeners/support-notification.listener.tsEmail notifications for ticket events
src/support/constants/support.constants.tsPubSub channels, events, defaults
src/support/dto/support-enums.tsEnum registrations
src/support/dto/support-ticket.input.tsInput types
src/support/dto/faq.input.tsFAQ input types
src/support/dto/support-ticket.output.tsOutput types
  • Support Frontend Guide — Complete GraphQL operations, page structure, and component breakdown for frontend implementation