Support

Run a helpdesk on your own site: a ticket portal your customers use, an agent inbox in the admin, and replies that arrive without a refresh.

Support turns your WordPress site into a helpdesk. Customers open tickets on a page you build, read the thread and reply. Your team works the same tickets from wp-admin, with internal notes the customer never sees, statuses you name yourself, and the customer's plan and licences beside the conversation.

Both screens keep themselves current. With no third-party account they check for new activity every five seconds. With Realtime configured for Pusher or PubNub they update over a socket in about a tenth of a second, and the timer stops.

Where to find it

Two places. The customer's half is the Support Portal block, which you put on a page. Your half is XPAC → Support in wp-admin, which appears for anyone whose role holds the agent capability. Everything site-wide lives in XPAC → Support → Settings.

Quick start

Activate the plugin. It creates its own tables on activation; nothing else happens yet.
Create and publish a page — "Support" is the usual name — and add the Support Portal block to it.

Open XPAC → Support → Settings → Portal and choose Who can open a ticket. Signed in is the safe default. Anyone makes the composer on that page a public write endpoint; read Before you launch first.

Under Email, set the From name and From address, or leave them empty to use your site title and WordPress's default address. Support sends through WordPress mail, so an SMTP plugin your site already uses delivers it too.

Give your team the agent capability. Any role holding xpac_support_respond sees the inbox; by default that is anyone who can manage options.

Open the portal page while signed out of your admin account and file a real ticket. Answer it from the inbox, and check that the emails arrive.

The two surfaces

Customer portalAgent inbox
WhereA page on your site carrying the Support Portal blockXPAC → Support in wp-admin
WhoSigned-in customers, and guests if you allow themRoles holding xpac_support_respond
SeesTheir own tickets, public replies onlyEvery ticket in scope, replies and internal notes
CanOpen a ticket, reply, attach files, close their ownReply, note, set status, priority, product, close, reopen, delete
NeedsNothing beyond the blockThe capability, and the inbox menu appears

The portal is one block used three ways. Its mode attribute is full for the list and thread, and compose for a bare "open a ticket" form you can drop on a contact page. The same block also mounts inside the account area of a site running the XPAC account plugin, where the navigation shows an unread count beside it.

Tickets

A ticket is a subject, a first message and everything that happened afterwards.

FieldWhat it is
ReferenceA twelve-character public id like A7K2M9QX3B4C. Customers quote it; searching for it is the fastest lookup in the product
SubjectThe customer's own words. Searchable
StateWhat the machine reasons about: open, pending, on hold, resolved, closed, spam, trash
StatusYour own label on top of a state, so "pending" can read "Waiting on customer" without a database change
PriorityYour own list. Ordering only; it drives no automatic behaviour on its own
ProductWhich of your things this is about. Optional, and you define the list
RequesterThe person who opened it — an account, or an email address for a guest
ParticipantsThe requester plus anyone CC'd. A participant sees the ticket; nobody else does

Replies come in two kinds and one axis crosses both. A reply is part of the conversation; a note is for your team. A system entry is written by the plugin itself — "Resolved by Ani", "Dana assigned this to Miles" — and it can be public or internal too. All four combinations are real, which is why visibility and kind are stored separately.

Attachments are stored outside the media library and served through a route that checks access on every single request, so a file's URL is not a way around permissions. Size, count per reply and permitted MIME types are all settings.

Who can see what

Three questions get asked on the server on every read, every write and every live update, and the answer to all three comes from one place:

  1. Is this person an agent? A capability check, filterable, never a role name.
  2. May they see this ticket? A requester is a row in the participants table. An agent is the capability, and an agent sees every ticket on the site.
  3. May they see this reply? Public replies for everyone on the ticket; internal notes for agents only.

An internal note is not filtered out of a customer's page — it never reaches it

Notes are stored with a different visibility value, counted by a different counter and published on a channel the customer cannot subscribe to. The customer is not shown the note, and is not told that a note exists: the number their browser polls does not move when an agent writes one.

Search obeys the same rule, and it matters more than it looks. A customer searching their own thread never matches text inside an internal note — not even to change the number of results — because the visibility ceiling is applied inside the search, not after it.

Live updates

Three tiers, and every install gets one of them:

No Realtime pluginRealtime, polling providerRealtime, Pusher or PubNub
How it stays currentSupport's own counter routeRealtime's batched counter routeA socket
Latency5–15s, whatever the interval says5–15sAbout 100ms
Requests per tick, per browser11, shared with every other XPAC feature on the page0
Hidden tabsNoInbox only, at a quarter rateYes, everywhere
Typing indicator——Yes
"Who else is viewing this"——Yes, agents only
Third-party accountNoneNonePusher or PubNub

Polling is not a fallback. It is the tier most sites run, and the one every install gets without signing up for anything. The browser asks a twenty-byte question — "has forty-one become forty-two" — rather than reloading anything, and it reacts once per change no matter how many things changed. Six replies inside one five-second window produce one refresh, not six.

The floor is five seconds and it is enforced in three places, so no setting can go under it. An agent inbox with a ticket open costs one request and about two database queries every five seconds from one browser, however long the ticket list is: the list rides one counter for the whole site rather than one per ticket.

That has an aggregate you should size before you promise it. Twenty agents plus a hundred customers with the portal open, all at the five-second floor, is roughly 24 requests a second reaching WordPress. On a shared host that is real load. XPAC → Support → Settings → Live prints your own site's figure from your agent count and your interval; raise the interval to fifteen seconds and it drops to a third.

A dead socket falls back, it does not go quiet. Sockets get refused for ordinary reasons — an agent whose capability was taken away, an expired page nonce on an inbox left open across a shift, a security plugin blocking a REST request. Every screen carries one chip that tells you which tier you are on and whether it is working:

  • Live — a filled dot. A socket is carrying.
  • Checked 4s ago — a hollow ring, counting up. Polling, and the last check succeeded.
  • Not updating — Reload — amber. Three failures in a row, or a refused request.

The chip is a button; clicking it forces a check. The third state exists because the alternative ships: a screen that looks perfectly alive and has been dead since lunchtime.

New activity arrives as a New reply · Show bar rather than appearing under your cursor, except in a thread you are already reading at the bottom of a focused tab. Sending is never gated on any of this — your own message posts over REST and appears immediately.

Where tickets come from

RouteNeedsNotes
The portal composerThe block on a pageThe normal path
A formXPAC Forms activeMap an existing form's fields to a ticket. The entry is stored first, then the ticket opens carrying its id
An agentThe inboxOpen a ticket on a customer's behalf

The Forms route is worth knowing about even if you already have a portal page: an existing contact form becomes a ticket source by mapping four fields, and the person filling it in sees no change at all.

Notifications

Support sends five emails. Each one has its own switch in Settings → Email, and all five are on by default.

EmailGoes to
Ticket openedThe customer: the requester and anyone copied in on the ticket
Agent repliedThe customer
Status changedThe customer
New ticketAgents
Customer repliedAgents

Agent emails go to the ticket's assigned agent, or to every agent while nobody is assigned. Whoever caused an event is not emailed about it; the one exception is the customer's own "ticket opened" confirmation.

An internal note never sends email, and neither does anything on a ticket marked spam or moved to the trash.

Mail is sent with wp_mail, like Forms and the rest of XPAC, so delivery is whatever your site already uses — for example an SMTP plugin such as WP Mail SMTP. Sending runs in the background on Action Scheduler, so a slow mail server never slows a reply down; on a site with no queue it sends during the request. A send that fails shows up in Action Scheduler's log with the reason wp_mail gave.

Every email has an HTML and a plain-text version, carries the ticket reference in its subject and threads by ticket in mail clients. Replying to an email does not add to the ticket, and the email says so: customers answer through the link to the ticket.

What you get

Support is a Pro plugin in its entirety. It is not sold a feature at a time and there is no free edition to compare it against: everything on this page — portal, inbox, replies, internal notes, attachments, statuses, priorities, products, guest tickets, live updates, assignment, canned replies, email notifications, and the privacy export, erasure and retention tools — is what the plugin is.

It is distributed to subscribers from xpac.io rather than published on wordpress.org, so you install it from a zip and it updates through the XPAC updater.

Support contains no licensing code

There is no licence check anywhere in this plugin, no gated feature, no 402, and no screen that asks you for a key. Install it on a site with nothing else from XPAC and every feature described here works. Licensing is an integration, not a layer: the XPAC account plugin supplies the licence screen, the updater and the telemetry, and Support only notices it to the extent of putting a Support section in the account area and showing the customer's plan beside their ticket. Remove that plugin and Support keeps working; nothing is disabled, expired or withheld.

Assignment

A ticket carries one assignee, or none.

Every agent sees every ticket. There is no scope below the capability: anyone holding xpac_support_respond can list, open and answer any ticket on the site. The list query and the live-update channel apply that same rule, so the REST API never shows an agent a ticket the inbox would hide, or the other way round.

The inbox offers Mine and Unassigned alongside its status views, plus bulk assignment from the list. Assignment is enforced twice on the server — on its own route and on the general ticket update route — because a gate on one route with the field still accepted on another is not a gate.

Canned replies

Named, reusable answers with placeholders for the customer's name, the ticket reference and the site name. Insert one from the composer, edit it before sending — nothing is sent as-is unless you send it as-is.

A shared reply is stored per site and offered to every agent, so an answer one agent writes well is the answer everybody sends. An agent can also keep personal replies that only they see.

Settings

XPAC → Support → Settings.

GroupWhat is in it
PortalThe portal page, who may open a ticket, guest tickets, how a guest claims a ticket after signing up, how long a guest session lasts
TicketsYour statuses, priorities and products
UploadsMaximum size, maximum per reply, permitted MIME types
LiveThe check interval, floored at five seconds, and this site's computed request rate
EmailFrom name (empty uses the site title), From address (empty leaves it to WordPress or your SMTP plugin), and a switch for each of the five emails
DataRetention, what retention applies to, whether uninstalling removes the tables, how long the audit log is kept
SpamCaptcha provider, and the honeypot and timing floor for guest submissions

Before you launch

Anyone-can-open-a-ticket is a public write endpoint

With Who can open a ticket set to Anyone, every page carrying the Support Portal block accepts writes from the internet. That is the correct setting for a shop whose buyers are not signed in, and the wrong one for most other sites. If you turn it on, turn on a captcha in Settings → Spam in the same sitting, and watch the first week.

Four more things to do before you send the page to customers:

Make sure mail arrives. Support sends through WordPress mail, so it is only as reliable as your site's mail. Many hosts deliver little or nothing without help, so install an SMTP plugin such as WP Mail SMTP if you do not have one. Then open a ticket as a customer, reply as an agent, and confirm each email arrives in an inbox rather than a spam folder.

Decide the attachment policy. The defaults are conservative — a short MIME allowlist and a few megabytes. Widen them deliberately.

Attachments are not scanned for malware

Uploads are limited by size, count and MIME type, stored outside the media library and served through an access-checked route. Nothing scans their contents. An agent opening a customer's attachment is opening a file from a stranger, and the plugin cannot make that safe.

Size the polling load. Read the number Settings → Live prints. If it is uncomfortable for your host, raise the interval before you have agents rather than after.

Say what you keep. Settings → Data sets retention. The privacy tools are wired up — Support answers WordPress's own export and erasure requests — but erasure destroys the requester's own reply bodies irreversibly, so the site owner's bulk export exists to keep a copy first.

For developers

Storage

Six tables, all prefixed wp_xp_support_: tickets, ticket_replies, ticket_participants, ticket_attachments, ticket_events and canned_replies. They are created and migrated by the plugin's own runner rather than by dbDelta guesswork. The current schema is 1.8.0; on an older site, M008_SimplifyMail removes the old mail system's tables, columns and settings, and M009_RemoveTags removes the ticket tag tables.

wp_xp_support_tickets is the hot one: twelve secondary keys, chosen so that every inbox view is an index range with the ordering already in the key and no filesort. Search is InnoDB FULLTEXT over the subject and the reply body, with a bounded LIKE fallback when the index could not be created and a straight unique-index hit when the term looks like a reference.

Nothing references wp_users with a foreign key. A ticket keeps the author's name and email as a snapshot written at insert, so deleting a WordPress account does not turn a thread into a wall of "user #0".

Blocks and assets

One block, xp/support-portal. Its front-end app is a Vite build declared as viewScriptModule, so WordPress registers and enqueues it as an ES module whenever the block renders. React is externalised to the shared XpacReact runtime the plugin ships. The agent inbox is a separate wp-scripts build on @wordpress/element, and the two share one TypeScript source file for the channel names, the event union and the liveness hook.

Request path

GET    /wp-json/xpac/v1/support/tickets
POST   /wp-json/xpac/v1/support/tickets
GET    /wp-json/xpac/v1/support/tickets/<id>
POST   /wp-json/xpac/v1/support/tickets/<id>/replies
GET    /wp-json/xpac/v1/support/attachments/<id>
GET    /wp-json/xpac/v1/support/revision?ticket=<id>
POST   /wp-json/xpac/v1/support/revisions

GET    /wp-json/xpac/v1/support/admin/tickets
PATCH  /wp-json/xpac/v1/support/admin/tickets/<id>
POST   /wp-json/xpac/v1/support/admin/tickets/<id>/replies

The portal routes and the agent routes are two controllers on purpose: the portal's response schema has no visibility field at all, so an internal note has nowhere to leak to even if a query forgets.

Channels

Seven namespaces, registered in one place:

NamespaceKeyed onAudience
xp-support-ticketticket idRequester, CCs and permitted agents
xp-support-ticket-staffticket idAgents only
xp-support-typingticket idSame as the ticket channel
xp-support-viewingticket idAgents only, presence
xp-support-inboxsite idEvery agent on the site
xp-support-customeruser idThat one customer
xp-support-agentuser idThat one agent

What travels is a signal — an event name, a role, an integer id and a timestamp, about ninety bytes. Never a subject, never a body, never a name. When sockets are on, every byte of this crosses a third party's infrastructure, and a support ticket is the richest personal data a WordPress site holds.

Email

Each email is rendered from an HTML and a plain-text template, placed inside a shared wrapper. The templates live in packages/Support/Templates/Notifications/:

EmailTemplates
Ticket opened (customer)ticket-created-html, ticket-created-text
Agent repliedagent-replied-html, agent-replied-text
Status changedstatus-changed-html, status-changed-text
New ticket (agents)new-ticket-html, new-ticket-text
Customer repliedcustomer-replied-html, customer-replied-text
Around every emailwrapper-html, wrapper-text

A theme can override any of them with a file at xpac-support/notifications/<name>.php. Sending runs on the Action Scheduler hook xpac_support_send_notification, in the xpac-support group. The $event these filters receive is one of ticket_created, agent_replied, status_changed, new_ticket or customer_replied.

FilterArgumentsWhat it controls
xpac_support_notification_enabledbool $enabled, string $event, int $ticketId, array $contextWhether one email is sent. It is only asked when that email's switch is on, so it can turn an email off but never on
xpac_support_notification_recipientsarray $recipients, string $event, array $ticketWho the email goes to. Each entry has email, name and user_id; an agent email never reaches the requester
xpac_support_notification_emailarray $context, string $event, array $ticket, array $recipientThe values the templates are rendered with
xpac_support_notification_templatestring $path, string $name, array $contextThe file a template is rendered from. It wins over both the theme and the packaged copy

Hooks

Lifecycle (xpac_support_init), ticket lifecycle (xpac_support_ticket_created, _ticket_status_changed, _reply_created, _note_created), policy (xpac_support_is_agent, _can_view_ticket, _can_reply_to_ticket, _ticket_statuses, _login_url), intake (_intake_ticket_args, _requester_for_submission), email (_notification_enabled, _notification_recipients, _notification_email, _notification_template), the account-area integration (xpac_support_customer_context and the xpac_account_sections filter it registers on), and the portal block (xpac_support_portal_agent_name). In the browser, @wordpress/hooks filters xpac-support-inbox-columns, xpac-support-ticket-panels, xpac-support-reply-toolbar and xpac-support-portal-ticket-actions.

The portal filter is the one a site's own glue plugin is most likely to need:

FilterArgumentsWhat it controls
xpac_support_portal_agent_namestring $name, int $userIdThe name customers see on an agent's replies and in the "Name:" prefix of a ticket's last-reply line. The default is the agent's first name, then their display name when it differs from their login, then empty; an empty name reads as "Support team"

See the Support hook reference for every hook with its signature and call site.

On this page