support

Support tickets with priority, status, and SLA tracking

Support

Support tickets linked to contacts, with priority levels, status tracking, and SLA metrics.

Tables

tickets

ColumnTypeDescription
iduuidPrimary key, auto-generated
org_iduuidReferences organizations. CASCADE on delete
contact_iduuidReferences contacts. SET NULL on delete
assignee_iduuidReferences users. SET NULL on delete
creator_iduuidReferences users. SET NULL on delete
titletextTicket title (required)
descriptiontextOptional ticket description
statusticket_statusCurrent status, defaults to 'open'
priorityticket_priorityPriority level, defaults to 'medium'
channelchannel_typeHow the ticket was created (email, sms, etc.)
resolved_attimestamptzWhen the ticket was resolved
closed_attimestamptzWhen the ticket was closed
first_response_attimestamptzFor SLA tracking
created_attimestamptzRow creation time
updated_attimestamptzAuto-updated on change via trigger
metadatajsonbFreeform JSON, defaults to empty object

Enums

ticket_status

ValueDescription
openNew or reopened, needs attention
pendingWaiting on customer or third party
resolvedSolution provided
closedTicket closed, no further action

ticket_priority

ValueDescription
lowLow priority
mediumMedium priority (default)
highHigh priority
urgentNeeds immediate attention

channel_type

ValueDescription
emailEmail channel
smsSMS / text message
chatLive chat or messaging
phonePhone call
socialSocial media

Row-Level Security

RLS is enabled and scoped to org_id via get_user_org_id(). SELECT, INSERT, and UPDATE are open to all org members. DELETE requires admin privileges via is_admin().

Dependencies

  • crm (contacts, companies)

Example Queries

-- Open tickets sorted by priority and age
SELECT
  t.title,
  t.priority,
  t.channel,
  c.first_name || ' ' || coalesce(c.last_name, '') AS contact,
  u.full_name AS assignee,
  extract(epoch FROM now() - t.created_at) / 3600 AS hours_open
FROM tickets t
LEFT JOIN contacts c ON c.id = t.contact_id
LEFT JOIN users u ON u.id = t.assignee_id
WHERE t.status IN ('open', 'pending')
ORDER BY
  CASE t.priority
    WHEN 'urgent' THEN 0
    WHEN 'high' THEN 1
    WHEN 'medium' THEN 2
    WHEN 'low' THEN 3
  END,
  t.created_at ASC;
-- Average first response time by priority (last 30 days)
SELECT
  t.priority,
  count(*) AS ticket_count,
  round(
    avg(extract(epoch FROM t.first_response_at - t.created_at)) / 3600, 1
  ) AS avg_response_hours
FROM tickets t
WHERE t.first_response_at IS NOT NULL
  AND t.created_at >= now() - interval '30 days'
GROUP BY t.priority
ORDER BY
  CASE t.priority
    WHEN 'urgent' THEN 0
    WHEN 'high' THEN 1
    WHEN 'medium' THEN 2
    WHEN 'low' THEN 3
  END;
-- Ticket volume by channel and status
SELECT
  t.channel,
  count(*) FILTER (WHERE t.status = 'open') AS open,
  count(*) FILTER (WHERE t.status = 'pending') AS pending,
  count(*) FILTER (WHERE t.status = 'resolved') AS resolved,
  count(*) FILTER (WHERE t.status = 'closed') AS closed,
  count(*) AS total
FROM tickets t
GROUP BY t.channel
ORDER BY total DESC;
-- Unassigned tickets ordered by priority
SELECT
  t.title,
  t.priority,
  t.channel,
  t.created_at,
  c.first_name || ' ' || coalesce(c.last_name, '') AS contact
FROM tickets t
LEFT JOIN contacts c ON c.id = t.contact_id
WHERE t.assignee_id IS NULL
  AND t.status IN ('open', 'pending')
ORDER BY
  CASE t.priority
    WHEN 'urgent' THEN 0
    WHEN 'high' THEN 1
    WHEN 'medium' THEN 2
    WHEN 'low' THEN 3
  END,
  t.created_at ASC;
-- Average resolution time by assignee (last 30 days)
SELECT
  u.full_name,
  count(*) AS resolved_count,
  round(
    avg(extract(epoch FROM t.resolved_at - t.created_at)) / 3600, 1
  ) AS avg_resolution_hours
FROM tickets t
JOIN users u ON u.id = t.assignee_id
WHERE t.resolved_at IS NOT NULL
  AND t.resolved_at >= now() - interval '30 days'
GROUP BY u.id, u.full_name
ORDER BY avg_resolution_hours ASC;