Workshop Chat

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}/joinParticipant → Controller
phpgrn/{PIN}/msgParticipant → Controller
phpgrn/{PIN}/leaveParticipant LWT / explicit leave
phpgrn/{PIN}/broadcastController → 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

💬 Participant A
browser
MQTT over WSS
(publish / subscribe)
☁️ MQTT
Broker
broker.emqx.io
MQTT over WSS
(publish / subscribe)
🎛️ Controller
browser / beamer

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

  1. 1
    Controller connects to the MQTT broker On session start the controller generates a random 5-digit PIN and connects to the MQTT broker (broker.emqx.io by default, or a custom broker). It subscribes to the topics phpgrn/{PIN}/join, phpgrn/{PIN}/msg, and phpgrn/{PIN}/leave. A Last Will message is registered so participants are notified if the controller disconnects unexpectedly.
  2. 2
    QR 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.
  3. 3
    Participant scans the QR code / types the PIN The participant's browser connects to the MQTT broker, subscribes to phpgrn/{PIN}/broadcast and a personal DM topic phpgrn/{PIN}/dm/{participantId}, then publishes a join message with their display name and reconnect token.
  4. 4
    Controller 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.
  5. 5
    Controller 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 to phpgrn/{PIN}/broadcast so all participants see them. This keeps the controller in full control and simplifies history sync for late joiners.
  6. 6
    Session ends — everything is wiped When the controller closes the tab a session-end message 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

No server-side storage. This app has no database, no backend, and no analytics. All message history lives in the controller's browser tab and is gone the moment the session ends.
Transport is encrypted in transit. All MQTT traffic uses TLS over WebSockets (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.
Public broker notice. The default broker (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?

Data Seen by MQTT broker Stored anywhere
Message content Yes — routed through broker No (retain: false)
Code snippets Yes — routed through broker No
Display names Yes — in join payload Browser localStorage only (reconnect token)
Room name No (PIN only, not name) Browser history & GitHub Pages access logs (via URL)
MQTT client IDs Yes — connection metadata Not by this app
IP addresses Yes — TCP connection origin Not by this app
Session history No Gone when controller tab closes
Team colour assignments Yes — broadcast to all participants No

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 localStorage under phpgrn-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-end to 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:

MQTT messaging
MQTT.js

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 License
Default MQTT broker
EMQX

Provides 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.

Free public tier
Syntax highlighting
Highlight.js

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 License
QR code generation
QRCode.js

Generates 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 License
CDN
cdnjs

Free 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 / Open
CDN (MQTT.js)
unpkg

A fast, global CDN for npm packages. Used to load the MQTT.js browser bundle directly from the npm registry.

Free / Open
Hosting
GitHub Pages

Serves 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
Open Workshop Chat Back to Workshops