A tiny live chat for the beta of a new project: visitors chat from a widget, you answer from one page in the control panel. Built on Datastar and server-sent events, it runs inside your Craft install, with no third-party service and no cookies.
- Craft CMS 5.6+, PHP 8.2+
- Datastar comes along as
putyourlightson/craft-datastar-module; the Datastar plugin is optional
Until the first release is on Packagist, add the repository to your project's composer.json:
"repositories": [
{ "type": "vcs", "url": "https://github.com/larsmarkus-studio/craft-quickchat.git" }
]composer require larsmarkus-studio/craft-quickchat
php craft plugin/install quickchatPut the launcher in your layout, before </body>:
{{ craft.quickchat.widget() }}Then set office hours, the support email and the texts under Settings → Plugins → Quickchat
(also linked from the status badge on the dashboard), or in config/quickchat.php (every property
of src/models/Settings.php, multi-environment aware).
Plugin settings are project config: with allowAdminChanges off in production, change the hours
locally and deploy, or keep them in config/quickchat.php per environment.
- Office hours: outside them, visitors see your support email instead of the chat. Open conversations stay readable and writable, with an "offline" note.
- Optional email before the first message, so you can follow up after the visitor leaves.
- Live both ways: the widget and the CP dashboard check for new messages every few seconds (see Polling or held streams).
- Chime in the CP for new visitor messages, switched on per browser on the dashboard. Browsers only allow sound after you've clicked the page once; the dashboard says so if it was blocked.
- Unread badge on the CP nav item, resolve / reopen per conversation. A visitor writing in a resolved chat reopens it.
- Mail per new conversation to
notifyEmail(optional). - Multi-site: each chat stores the site it started on, the CP shows it on one shared page,
and visitor texts follow the site's language (
translations/<lang>/quickchat.php, Dutch included). - Page URL the chat started on, shown in the CP.
- On page load: the launcher button, a few lines of inline CSS and
datastar.js, only on pages with the widget. Load your own Datastar (e.g. Pro)? SetregisterScript => falseinconfig/datastar.php. - On the first click: the panel and its stylesheet arrive over SSE. The stylesheet is Bootstrap 5.3
cut down to modal, close button, buttons and form controls, scoped under
.qcwith--qc-*variables, so it never touches your own CSS (~4 KB gzipped). - No JavaScript besides Datastar. Signals live under
qc, so they can't clash with yours. - The panel uses your site's body font. Accent colour and launcher font: set
--qc-accentand--qc-launcher-fonton.qcin your own CSS.
Quickchat sets no cookies. The conversation key is a random token in the visitor's localStorage
and is sent in POST bodies only, never in URLs. The visitor endpoints skip CSRF on purpose: CSRF
protects cookie-based sessions, and these endpoints have none. Starting a chat is limited to five per
IP per ten minutes and messages to one per second (through Craft's cache, where the IP sits hashed
for those ten minutes; nothing about the visitor is stored besides what they type). Behind a load
balancer or CDN, tell Craft which proxy headers to trust (trustedHosts, ipHeaders on the
request component in config/app.php), or every visitor shares the proxy's IP and its limit.
By default the widget and the dashboard check for new messages every pollSeconds (3). Each check
is a short Datastar request that answers at once (about 50 ms), so it never ties up a PHP process
and works behind any proxy, buffering or compression. A message shows up within a few seconds.
On a server with PHP processes to spare, set holdSeconds (e.g. 30) for instant delivery: each
request then stays open that long and pushes messages the moment they arrive. Every open chat and
every open dashboard holds one PHP process for as long as it's open, and the server must pass
streamed responses through unbuffered. If messages arrive in bursts, something buffers them. On
Apache with PHP-FPM (DDEV's default), add .ddev/apache/sse-flush.conf:
<Proxy "unix:/var/run/php/php-fpm.sock|fcgi://localhost">
ProxySet flushpackets=on
</Proxy>Nginx needs nothing: the response sends X-Accel-Buffering: no.
php tests/hours.php # office-hours logic, no Craft needed
npm install && QUICKCHAT_URL=https://my-site.ddev.site/ npm run e2e # Playwright, against a page with the widgetThe e2e test checks that the stylesheet loads lazily, that a message round-trips and
survives a reload, and that no quickchat response sets a cookie. Run it locally or on staging: it
leaves an e2e … conversation behind.
npm install && npm run build recompiles src/web/quickchat.css from build/quickchat.scss.
python3 build/ding.py regenerates the chime, src/web/ding.wav.
The compiled file is committed, so installing the plugin needs no Node.
Design decisions are in docs/decisions.