-
Notifications
You must be signed in to change notification settings - Fork 15
Web Chat Developer Guide
The engineering detail behind the Web chat channel: the tables, the public endpoints, the security model in code, how it reuses the WhatsApp channel spine, and β because it's a work in progress β a precise resume plan for the parts still to wire.
Mental model. A web chat widget is the self-hosted twin of a WhatsApp number. It drives one
messaging_channelsrow (channel_type = 'webchat',provider = 'freeitsm'). There is no external provider and no credentials; "sending" a reply just persists a row that the visitor's browser polls for. Everything downstream β ticket creation, theemailsthread, the reading-pane composer, multi-tenancy routing β is reused unchanged. If you're touching this, the first instinct should always be "how does WhatsApp already do this?"
Four tables. webchat_widgets and webchat_conversations are live; webchat_messages exists but is only used by the (not-yet-wired) AI runtime.
One row per widget, joined 1:1 to its messaging_channels row (channel_id, unique). Company routing and the active flag live on the channel row; this table holds only what the browser needs plus the behaviour toggles.
| Column | Purpose |
|---|---|
channel_id |
FK β messaging_channels (unique, ON DELETE CASCADE). |
widget_key |
Public key in the embed snippet (unique). Not a secret. |
allowed_origins |
Newline/comma list of permitted site origins. Empty = any (dev only). |
greeting, accent_colour, launcher_text, offline_message
|
Look & copy. |
require_email |
Pre-chat name+email gate (default 1). |
business_calendar_id |
FK β sla_calendars (ON DELETE SET NULL). NULL = always open. (runtime pending)
|
email_when_away |
Email a reply to the visitor if they've left. (runtime pending) |
ai_enabled, ai_mode (assist|deflect), ai_offer_agent, ai_offer_email
|
AI answers + escalation. (runtime pending) |
| Column | Purpose |
|---|---|
channel_id |
FK β messaging_channels (ON DELETE CASCADE). |
token |
The per-conversation capability (unique), held in the visitor's browser. |
ticket_id |
Set lazily on the first message (AI-off) or on escalation (deflect). One conversation β one ticket. |
visitor_name, visitor_email
|
From the pre-chat form. |
visitor_ip |
Rate limiting only. |
created_datetime, last_activity_datetime
|
Timestamps (written with UTC_TIMESTAMP()). |
The chat held before (and if) a conversation becomes a ticket β used in AI deflect mode, where the AI can answer without ever raising a ticket. sender β visitor|ai|agent|system. source_email_id is the dedup key when an analyst reply (in emails) is mirrored in so the widget can show it. Not written for plain AI-off widgets (those go straight to the ticket's emails thread).
-
messaging_channelsβ the channel spine (channel_type='webchat',provider='freeitsm'). -
emailsβ inbound/outbound messages,channel='webchat',channel_idset. The analyst's canonical thread. -
ticket_originsβ a seeded "Web chat" origin. -
sla_calendars(+_hours,_holidays) β office hours. -
knowledge_articlesβ the AI answer source.
-
FreeitsmProvider(includes/messaging/FreeitsmProvider.php, registered inmessagingProvider()): satisfies the sharedMessagingProvidercontract.sendMessage()does no network I/O β it just mints a synthetic id; the outboundemailsrow is what the visitor polls. This is why the existingapi/messaging/send_message.phpreply path works for webchat with no special-casing. -
webchatIngestMessage()(includes/webchat/webchat.php): the webchat twin of the WhatsApp ingest. Creates/append the ticket, stores the message inemailswithchannel='webchat', seeds the requester from the real visitor email. Threading is by the conversation'sticket_id, not byfrom_address(unlike WhatsApp, whosenormaliseChannelIdentifieris phone-shaped). -
24-hour window bypass: webchat has no provider service window, so it's exempt in both
send_message.phpandget_ticket_thread.php($ticketChannel === 'webchat'β always "open"). -
"Web chat" ticket origin + subject label:
getChannelOriginId()andbuildChannelSubject()special-casewebchatβ'Web chat'.
All under api/webchat/, unauthenticated (a website visitor hits them), and all routed through the guard in includes/webchat/public.php.
| Endpoint | Method | Does |
|---|---|---|
config.php |
GET | Returns the widget's public look-and-feel for widget.js. |
start.php |
POST | Creates a conversation, returns the token. No ticket yet. |
send.php |
POST | Ingests a visitor message β ticket (AI-off flow, live). |
poll.php |
GET | Returns messages since after id β the delivery mechanism. |
escalate.php |
POST | Planned β "talk to a person" / "raise a ticket by email". |
widget.js is a static, dependency-free file served from the same directory; it reads its own key + API base from its <script> tag and renders the launcher + panel in a Shadow DOM.
includes/webchat/public.php is where the safety lives:
-
webchatPublicResolve($conn, $key)β loads the widget by key, checks it's active, enforces the origin allowlist, sets scoped CORS headers, or exits with JSON. Treat its return as authorised. -
webchatRequestOrigin()βOriginheader, falling back to theRefererorigin when absent (same-origin embeds send noOrigin; this bit us in the local demo where the site and FreeITSM were bothhttp://localhost). The allowlist is a deterrent, not crypto. -
webchatLoadConversation($conn, $token, $channelId)β a token is only valid for its own widget's channel; a token from widget A can't be replayed on widget B. -
webchatRateLimitStart()/webchatRateLimitSend()β per-IP new conversations and per-conversation messages, per minute.
The rule to remember: the widget key only lets you start a conversation from an allowed origin. Reading or posting an existing conversation needs its token. So the key leaking (it always does β it's in page source) never exposes anyone's chat.
widget.js polls poll.php?after=<lastId> every ~3s. poll.php returns both directions (so a reload rebuilds the transcript), plus a closed flag from the ticket's status so the widget can show a "conversation closed" state. SSE is the obvious later upgrade; the widget already funnels all rendering through one poll(), so it's a contained change.
The groundwork is committed (includes/knowledge/kb_ai.php, includes/webchat/ai.php); the wiring is the next build.
-
Retrieval β
kbRetrieveArticles()+kbBuildContext()(shared, inkb_ai.php): embedding search over publishedknowledge_articles(falls back to all articles if no OpenAI key/embeddings). (Knowledge isn't multi-tenant yet, so no company filter β add one here when it is.) -
Answer β
webchatAiReply($conn, $question, $history)(ai.php): builds a strictly-grounded, KB-only system prompt and callsaiProviderChat()with the sharedknowledge_aiprovider/key. Returns{ok, answer, articles}. -
Assist vs deflect β
- Assist: ticket created on first message (current flow); AI answer posted alongside. Nothing is lost.
-
Deflect: no ticket until escalation. Visitor + AI messages live in
webchat_messages. The AI always offers escalation.
-
Escalation (
escalate.php, to build) β- agent: promote the conversation to a live ticket (transcript becomes the opening message), then it behaves like a normal webchat ticket.
-
email: create a ticket with an AI summary as the body and the full chat log as a
.txtattachment (reusesaveChannelMediaAttachment()from the ingest), then tell the visitor it'll be answered by email.
-
Transcript unification β when
ai_enabled, the widget reads fromwebchat_messages. Analyst replies (stored inemails) are mirrored intowebchat_messagesat poll time, deduped bysource_email_id, so the visitor sees them without the ticket/emails becoming the widget's source of truth.
Reuse sla_load_calendar() from includes/sla.php. A small webchatIsOpenNow($conn, $calendarId) (to write) evaluates the calendar's weekday hours + holidays in its timezone; NULL calendar β always open. When closed, send.php shows the offline message and takes the enquiry as a ticket to answer later (no live expectation).
The one genuinely mailbox-dependent piece. Direction matters: outbound goes from the company's own service-desk mailbox (the same authenticated Microsoft/Google/SMTP mailbox as email tickets) to the visitor β it never sends as the visitor's address. Because a webchat ticket has no source mailbox (it didn't arrive via one), the sender is the company's configured mailbox; if the company has none, the toggle can't act, and Settings should say so. Reuse the senders in api/tickets/send_email.php (sendEmailViaGraph() / the SMTP path) β but note getMailboxForTicket() there assumes an email ticket, so mailbox selection for a webchat ticket needs its own small resolver (company β mailbox). Cannot be verified locally without sending real mail, so build it carefully and test against a real mailbox.
includes/webchat/webchat.php core helpers: key gen, embed snippet, origin parse/allow,
load-by-key/channel, get-or-create user, webchatIngestMessage
includes/webchat/public.php the public guard: resolve, origin (+Referer), token load, rate limits
includes/webchat/ai.php webchatAiReply() (groundwork, not yet called)
includes/knowledge/kb_ai.php shared KB retrieval (embedding search + context)
includes/messaging/FreeitsmProvider.php no-op provider for the shared reply path
api/webchat/config|start|send|poll.php public endpoints (escalate.php = to build)
api/webchat/widget.js the embeddable widget (Shadow DOM)
api/webchat/get_widgets|save_widget|delete_widget.php authed settings API (Cap::TICKETS_WEBCHAT)
tickets/settings/index.php the Web chat settings tab + modal
database/freeitsm.sql Β· api/system/db_verify.php schema (4 tables) + Web chat origin seed
In build order β each is testable locally except offline email:
-
webchat.phpruntime helpers:webchatIsOpenNow(),webchatAddMessage(),webchatGetMessages(),webchatTranscriptText(),webchatMirrorAgentReplies(),webchatPromoteToTicket()(+ an AI summary helper inai.php). -
config.php: also exposeai_enabled,ai_offer_agent,ai_offer_email, and the current open/closed state + offline message, so the widget can render the right UI. -
send.php: branch on open/closed β AI (assist/deflect) β fall back to the current ingest. In AI mode, write towebchat_messagesand return the AI answer. -
escalate.php(new):agentandemailroutes viawebchatPromoteToTicket(). -
poll.php: forai_enabledconversations,webchatMirrorAgentReplies()then readwebchat_messages; else the currentemailsread (unchanged). -
widget.js: AI answer bubbles, the two escalation buttons, and the offline/closed state. - Offline email (mailbox-dependent β do last, test against a real mailbox).
- Changelog + README +
git push; run Database Verify.
The AI provider is configured locally (OpenRouter, ~20 embedded articles), so steps 1β6 can be driven end-to-end with the local Dream Holidays demo site (c:\wamp64\www\dream-holidays\, not in git).
-
Same-origin sends no
Originheader β usewebchatRequestOrigin()(Referer fallback), never$_SERVER['HTTP_ORIGIN']directly. - A PHP fatal is served as HTTP 200 β always read the response body when testing, not just the status.
-
Testing POSTs with curl: use
--data-raw; a bare-dmangled a JSON payload during development. -
The panel is fixed-height β the message list must be a flex column with
min-height:0or the composer gets pushed off the bottom (already fixed; don't regress). - After any schema change here, update both
freeitsm.sqlanddb_verify.php, then run Database Verify.
- Web chat channel (feature overview) Β· WhatsApp channel (the sibling it reuses)
- Multi-Tenancy: Developer Guide Β· AI Providers Β· SLA Management Β· Mailbox Authentication
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)