About Workshop Chat
A fully static, serverless chat built for live PHP workshops — no sign-up, no database, nothing stored on a server. Messages are routed through a public MQTT broker over WebSockets.
Two roles
Controller
The workshop host opens the app and starts a room. A 5-digit PIN and QR code appear — these go on the beamer. The controller's view shows the full chat feed, a participant list, and lets the host assign team colours.
Participant
Attendees open /chat/?room=<name> (from the QR code or a link) and type the PIN. After choosing a display name they're live in the chat, able to send messages and share syntax-highlighted code snippets in PHP, JavaScript, Python, HTML, CSS, or plain text.
Technology
Workshop Chat is a fully static single-page application. There is no server-side code, no database, and no build step — just HTML, CSS and vanilla JavaScript modules served from GitHub Pages.
MQTT over WebSockets
MQTT (Message Queuing Telemetry Transport) is a lightweight publish/subscribe messaging protocol originally designed for IoT devices. Every browser connects to a shared MQTT broker over an encrypted WebSocket (wss://). Messages are published to named topics; subscribers receive everything published to topics they're watching.
We use it here because it works through any firewall or guest Wi-Fi — unlike peer-to-peer WebRTC, no direct browser-to-browser path is needed.
Topic scheme
All topics are scoped to the session PIN to avoid collisions between parallel workshops:
phpgrn/{PIN}/join | Participant → Controller |
phpgrn/{PIN}/msg | Participant → Controller |
phpgrn/{PIN}/leave | Participant LWT / explicit leave |
phpgrn/{PIN}/broadcast | Controller → all participants |
phpgrn/{PIN}/dm/{id} | Controller → one participant |
The controller's Last Will is a session-end broadcast, so participants are notified even if the host's browser crashes.
Star topology
Participants never talk directly to each other — all messages go through the controller. When a participant sends a message, the controller receives it, stores it in memory (for history), and re-publishes it to the broadcast topic. This gives the controller full visibility and makes late-join history sync straightforward.
Reconnect & identity
MQTT.js handles automatic reconnection with exponential back-off. Each participant gets a random UUID stored only in memory. A reconnect token (stored in localStorage keyed by PIN) lets participants re-join after a page refresh and keep their display name without being treated as a new user.
Syntax highlighting
Code snippets are highlighted client-side using Highlight.js with the GitHub light theme. Supported languages: PHP, JavaScript, Python, HTML, CSS, and plain text. The language grammar files are loaded from cdnjs — only the grammars actually used are included.
No framework, no bundler
The app is written in plain ES modules (import/export) loaded natively by the browser. No React, Vue, or bundler required. All state lives in a single shared state.js object, keeping the code easy to follow and debug.
Connection diagram
(publish / subscribe)
Broker
(publish / subscribe)
All traffic passes through the MQTT broker over an encrypted TLS/WebSocket connection. No direct browser-to-browser path is needed, so the app works on guest Wi-Fi with client isolation.
Step-by-step connection flow
-
1Controller connects to the MQTT broker On session start the controller generates a random 5-digit PIN and connects to the MQTT broker (
broker.emqx.ioby default, or a custom broker). It subscribes to the topicsphpgrn/{PIN}/join,phpgrn/{PIN}/msg, andphpgrn/{PIN}/leave. A Last Will message is registered so participants are notified if the controller disconnects unexpectedly. -
2QR code & join URL generated The app builds a join URL (
/chat/?room=name) and encodes it in a QR code entirely in the browser — no server call. If a custom broker is configured its URL is included as&broker=…so participants connect to the same broker automatically. -
3Participant scans the QR code / types the PIN The participant's browser connects to the MQTT broker, subscribes to
phpgrn/{PIN}/broadcastand a personal DM topicphpgrn/{PIN}/dm/{participantId}, then publishes ajoinmessage with their display name and reconnect token. -
4Controller admits the participant The controller validates the name/token, registers the participant, then publishes the full session history and a colour map to the participant's personal DM topic. The participant's UI unlocks and shows the chat.
-
5Controller relays messages to all participants The topology is a star: participants publish messages to
phpgrn/{PIN}/msg; the controller receives them, stores them in memory, and republishes tophpgrn/{PIN}/broadcastso all participants see them. This keeps the controller in full control and simplifies history sync for late joiners. -
6Session ends — everything is wiped When the controller closes the tab a
session-endmessage is published to the broadcast topic. Each participant clears its feed, resets state, and returns to the start screen. Nothing is written to disk anywhere.
Privacy & data
wss://), so messages are protected against eavesdropping on the network between your browser and the broker. Shared or guest Wi-Fi is safe from local sniffing.
broker.emqx.io) is a shared free-tier service operated by EMQX. The broker operator can read message payloads in transit. Additionally, anyone who guesses or knows your 5-digit PIN could subscribe to phpgrn/{PIN}/# and silently receive all messages. For a typical PHP workshop this is acceptable — nothing sensitive is shared. For sessions involving confidential code or personal data, use a private MQTT broker (enter its WSS URL in the Custom MQTT broker field).
What data leaves your browser, and where does it go?
What stays only in the browser?
- Message history — held in the controller tab's memory only; never written to disk by this app.
- Reconnect token — a random hex string saved in
localStorageunderphpgrn-token-{PIN}. Used only to let a participant reclaim their display name after a page refresh. Cleared when the host ends the session. - Participant ID — a random UUID generated at join time, kept in memory for the session lifetime. Never persisted.
Recommendations for sensitive workshops
- Self-host a Mosquitto or EMQX broker behind authentication and use the Custom MQTT broker option.
- Restrict broker access to the workshop venue's IP range.
- Choose a long, random session name — the PIN is only 5 digits, but the topic also includes the room name in the join URL.
- End the session (close the controller tab) promptly; this broadcasts a
session-endto all clients and clears their state.
Open-source libraries & services
This app is built entirely on open-source tools and free tiers. We gratefully acknowledge:
The de-facto JavaScript MQTT client. Used here in browser mode over WebSockets to connect to the MQTT broker. Handles reconnection, QoS, and Last Will messages automatically.
MIT LicenseProvides a free public MQTT broker (broker.emqx.io) suitable for testing and workshop use. For production use, configure a private broker via the Custom MQTT broker option.
Automatic syntax detection and highlighting for code snippets. Used here with the GitHub light theme for clean, readable code blocks in PHP, JavaScript, Python, HTML, CSS, and plain text.
BSD 3-Clause LicenseGenerates QR codes entirely in the browser using HTML5 Canvas. Used to encode the join URL so participants can scan it from the beamer without typing a long URL.
MIT LicenseFree CDN for open-source libraries, maintained by Cloudflare. Serves Highlight.js and QRCode.js. No tracking; files are delivered over HTTPS from global edge nodes.
Free / OpenA fast, global CDN for npm packages. Used to load the MQTT.js browser bundle directly from the npm registry.
Free / OpenServes the entire GroningenPHP website as a static site directly from the GitHub repository — no web server to manage and free for open-source projects.
Free for public repos