Technical detail
Protocol, error codes, accessibility and the status behind every promise.
This page is for people who want the detail. Everything here is measured or read off the source — none of it is estimated.
How the widget loads
Two stages. First a small loader that draws the button and does nothing else. The chat itself is fetched only when one of five triggers fires — a click on the button, a command from the page, a rule, scrolling, or a timer.
There is a reason: most visitors never open the chat. Nothing should be loaded for them that they do not need.
- Loader: 2.83 kB (brotli)
- Whole widget: 59.2 kB gzip, 51.8 kB brotli
- Until the button is there: 11–21 ms
- Layout shift: 0.0000 — the space is reserved before anything appears
For comparison, also measured: Crisp's loader is 2,876 B, Intercom's is 3,161 B. At 2,827 B we are level with them, not orders of magnitude below. Anyone claiming otherwise has not measured.
It does not disturb your site
The widget lives in a closed shadow DOM. Your styles cannot reach it, its
styles cannot reach your page — not even if you set
* { box-sizing: border-box } or use a CSS framework that redefines
everything.
It runs under a strict Content Security Policy without a single console
message: no eval, no web font, no tracking pixel. It sets its styles through
constructable stylesheets, which style-src does not cover. The button, the
chat window, product tours and the agent console on your site therefore
need no unsafe-inline — measured under style-src 'self' and even under
style-src 'none': zero violations, fully styled. Measured in current
Chromium; older browsers without constructable stylesheets (such as Safari
before 16.4) fall back to a <style> element, which a strict policy blocks.
Zero requests to other servers. Everything comes from the address you embed the widget from.
The protocol
The connection uses WebSocket. The protocol is versioned and defines **25 error codes** — each with a prescribed client response (retry, do not retry, close). An error not in the list is treated as "do not retry": better one message too few than an endless loop.
Reconnect with growing backoff. If the connection drops, messages go into a buffer and are sent as soon as it is back. Every message carries an id; the server recognises repeats and does not store them twice.
Accessibility
Built against WCAG 2.2 AA:
- Focus trap in the open window, ESC closes, focus returns to where it was
- New messages are announced via
aria-live - Contrast between text and surface is calculated, not assumed — see below
- Fully keyboard operable, visible focus ring
Contrast is calculated
You give a colour. The surface takes exactly that colour. The text on it is shifted until it reaches a ratio of 4.5:1 — the WCAG requirement for normal text.
Measured across 178 colours in two themes and three pairings, so 1,068 data points: not one below 4.5:1, the worst value 4.50:1.
That is the difference to other providers: there, a widget can be configured until nobody can read it.
Personal data
Masking happens in two places — in the visitor's browser before anything leaves the machine, and again on the server before anything is stored. Recognised are card numbers (with Luhn check), IBANs, e-mail addresses, phone numbers and long digit sequences.
Two places, because one is not enough: the browser one can be switched off, the server one only sees what arrives.
Rules
When the widget speaks up is defined in a declarative rule list — not in a condition in the code. Five rules ship by default (rage click, JavaScript error, purchase intent, exit intent, inactivity); your own can be added.
The difference is practical: changing a rule means changing a line, not a deployment.
Integrations
Webhook and Slack are done. For all others the provider is in the code, but most have never run against the real service.
Every integration runs through a queue with a circuit breaker: if a third party hangs, it is skipped for a while after repeated failures. A chat message is never held up by it — that is the most important rule in this layer.
What is not finished
So that nobody finds out while integrating:
- The MousePlayer bridge polls, because MousePlayer does not yet emit the agreed events by itself.
- REST API and agent interface are started, not finished.
Last updated: