Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lecture-chat

A transparent, click-through chat overlay for live lectures.

Students open a link and type. Their messages float over your slides Twitch-style and fade out. No side panel, no window chrome, nothing covering your deck.

Zero lines of server code. Zero database tables. Setup takes about five minutes.

Works on macOS, Windows and Linux.

The overlay sitting on top of a slide, with student messages floating in the corner


한국어 요약

강의 슬라이드 위에 학생 채팅이 트위치처럼 떠올랐다 사라지는 오버레이입니다. 서버 코드도 데이터베이스도 없이 정적 파일 + Supabase Realtime broadcast 만 씁니다.

  1. Supabase에서 새 프로젝트를 만들고 Project URLanon public key 를 복사합니다
  2. public/config.example.jsconfig.js 로 복사해 그 두 값을 채웁니다
  3. public/ 폴더를 아무 정적 호스팅에 올립니다 (Cloudflare Pages · Netlify · Vercel …)
  4. 학생에게 https://내주소/?room=강의이름 을 보냅니다
  5. 발표자 노트북에서 cd overlay-app && npm install && ROOM=강의이름 npm start

메시지는 어디에도 저장되지 않습니다. 강의가 끝나면 지울 것도 남지 않습니다. ?room= 값이 그대로 채널 이름이라, 학생 링크와 오버레이의 방 이름이 다르면 아무것도 안 뜹니다 — 안 보일 때 가장 먼저 확인할 지점입니다.

Windows PowerShell에서는 npm 대신 npm.cmd 를 쓰세요. 자세한 내용은 아래 영어 문서를 보시면 됩니다. UI와 코드 주석은 한국어입니다.


Architecture

flowchart LR
  S["Student phone / laptop<br/>index.html?room=my-class"]
  H["Static host<br/>Cloudflare Pages · Netlify …"]
  C["Supabase Realtime<br/>broadcast + presence<br/>channel: room:my-class"]
  E["Presenter laptop<br/>Electron app<br/>(127.0.0.1 internal server)"]
  O["Transparent overlay<br/>click-through · always on top"]

  S -. "① page load · once" .-> H
  S == "② message · WebSocket" ==> C
  C == "③ fan-out" ==> O
  E --> O
Loading

There are two separate paths, and that trips people up. The static host delivers HTML files once and has nothing to do with chat; messages travel entirely over Supabase Realtime. So there is no server for you to run.

The presenter's Electron app starts a tiny server bound to 127.0.0.1 and serves the overlay from there. This is not incidental: overlay.html imports supabase-js from a CDN as an ES module, and opening it as file:// makes that import fail CORS — the overlay then shows nothing, with no error.

The life of one message

sequenceDiagram
  participant Student
  participant SR as Supabase Realtime
  participant Overlay
  Student->>SR: broadcast { kind, name, text }
  Note right of Student: marked "sent" only after<br/>the server acknowledges
  SR-->>Overlay: to every screen on the channel
  Note right of Overlay: filter flooding, duplicates,<br/>blocked words
  Overlay->>Overlay: shown 14s, then gone
Loading

Messages are never persisted. No database, no migrations, no RLS policies, and nothing to clean up afterwards.

?room= becomes the channel name. The room in the student link and the room the overlay was started with must match exactly, or nothing appears. This is the most common failure.

Requirements

  • Node.js 18+
  • A Supabase account (free tier is plenty)
  • Somewhere to host static files (Cloudflare Pages, Netlify, Vercel, GitHub Pages — any of them)

Setup

1. Clone

git clone https://github.com/ggplab/lecture-chat.git
cd lecture-chat

2. Create a Supabase project

At supabase.comNew project. Copy the Project URL and the anon public key from Settings → API.

No SQL, no tables, no RLS configuration. Broadcast channels are public by default and work as-is.

Create a project dedicated to this. The anon key is meant to be visible in the browser. But if you reuse an existing project's key, every table in that project falls into anon's reach. This tool uses no tables at all, so with a dedicated project there is nothing to lose even if the key leaks. Never use the service_role key.

3. Fill in your config

cp public/config.example.js public/config.js       # macOS · Linux
copy public\config.example.js public\config.js     # Windows

Open public/config.js and set SUPABASE_URL and SUPABASE_ANON_KEY. That file is in .gitignore, so it can't be committed by accident.

4. Try it on one machine first

Double-click start.cmd (Windows) or start.command (macOS). It installs dependencies on first run, asks for a room name, and starts the overlay. Or from a terminal:

cd overlay-app
npm install
ROOM=test npm start          # Windows PowerShell: $env:ROOM="test"; npm.cmd start

The terminal prints [overlay] 로드: http://127.0.0.1:<port>/overlay.html?room=test. Open that same address with overlay.html swapped for index.html — that's the student page. Send a message and it floats up on the overlay.

On Windows PowerShell use npm.cmd, not npm. The default execution policy blocks loading npm.ps1, so npm fails with UnauthorizedAccess. npm.cmd skips the PowerShell shim and just works, without changing any security setting.

5. Deploy (the link students open)

Upload the public/ folder. There is no build step.

npx wrangler pages deploy public --project-name my-project   # Cloudflare Pages
npx netlify deploy --prod --dir public                       # Netlify

Send students:

https://your-domain/?room=2026-03-15-period1

Change ?room= per lecture to keep rooms separate. Pasted into Slack or a messenger, the link unfurls with a preview card.

How students join

Device What they get
Phone Opening the link is all that's needed. Add to Home Screen installs it as an app icon that opens without the address bar (PWA)
Computer A "작은 창으로" (open in a small window) button opens a compact 380×470 popup they can park next to their notes
On a phone Popped out on a laptop
Student page on a phone, showing the conversation and an emoji row The same page as a small popup window beside your notes

Students see the whole conversation, not just what they sent — someone at the back who can't read the projector still follows along. Emoji reactions stay projector-only; mixed into a narrow list they just make the text harder to read.

The popup is the same page with ?mini=1, so there is only one implementation to maintain. The button only appears on wide screens that have a mouse (any-pointer: fine), so touchscreen laptops still get it while phones don't.

When opened from a home-screen icon there is no ?room= in the URL, so the page remembers the last room it was used with.

Running the overlay

Control it with the icon buttons in the top-right of the overlay. Even while clicks pass through, hovering the toolbar makes the window accept clicks for that moment.

The overlay toolbar: lock, clear, backdrop minus, percentage, plus, next display, collapse

Icon What it does
🔒 Lock Pinned ↔ movable. Unlock to drag the window and resize it
🗑 Trash Clear the screen now. Wipes visible messages without killing the overlay
− / + Backdrop, 0%–85%. Fades a dark panel in behind the messages so they stay readable over busy slides. Text is never dimmed. Remembered between runs
🖥 Monitor Move the window to the next display
⏻ Power Quit. The window is click-through, so this is the only way to close it from the screen
‹ Collapse Fold the toolbar down to a single icon

Keyboard shortcuts exist as a backup. If another app already owns a combination, registration fails and the app tells you so in the terminal.

Key Action
Cmd/Ctrl + Alt + M Pinned ↔ movable
Cmd/Ctrl + Alt + X Clear the screen
Cmd/Ctrl + Alt + → Next display
Cmd/Ctrl + Alt + C Hide / show
Cmd/Ctrl + Alt + R Reset position and size

It opens on the external display by default — the projector your students see, not your laptop panel. It follows along if you plug or unplug a projector mid-lecture. The display list is printed at startup:

[overlay] 방: 2026-03-15-period1
  [0] 1920x1080 @(0,-1080)  scale 1x  — 확장 화면 ◀ 오버레이
  [1] 1440x900 @(0,0)  scale 2x  — 주 화면

What stays running, and for how long

Three things are involved and they have very different lifetimes. Nothing here is a server you have to babysit.

Where it runs How long it lives
Student page A static host (CDN) Always. No process, nothing to start or stop
Message delivery Supabase Realtime Always — but a free project can pause after a quiet week (see below)
The overlay Your own laptop Only while you're presenting

The terminal window is the overlay's lifetime. Close it, press Ctrl+C, or hit ⏻ on the toolbar, and the overlay is gone. Shutting down or logging out ends it too.

Closing the lid does not quit it — it sleeps. On wake, the WebSocket to Supabase may already be dead while the overlay still looks perfectly fine, so it reloads itself on resume and unlock-screen to get a fresh connection. The room and backdrop are in the URL, so nothing is lost.

Don't leave it running between classes. Start it when you start presenting; quit it when you're done. There is no daemon, no autostart, and no background service by design — one less thing that can be quietly broken the next time you walk into a room.

Environment variables

Name Meaning
ROOM Room name. Must match ?room= in the student link
SCREEN external (default) · primary · 0, 1, 2
OVERLAY_URL Load the overlay from a deployed URL instead of the built-in server

macOS notes

  • Resize from the grip in the bottom-right corner. Transparent frameless windows on macOS don't support edge-drag resizing, so the grip is drawn inside the window instead.
  • Next display is Cmd+Alt+→. Cmd+Opt+D was deliberately avoided — that's macOS's Hide Dock.
  • If shortcuts don't fire, grant your terminal (or Electron) permission under System Settings → Privacy & Security → Accessibility. Screen Recording is not required.
  • If the overlay won't sit above a Keynote presentation, switch Keynote to "Play in Window", or use the OBS route below.

Zoom, recording, streaming

In OBS, add a Browser Source on top of a Display Capture and point it at https://your-domain/overlay?room=…. Browser sources are transparent by default, so it composites directly. The toolbar only exists inside the Electron app, so it never appears on the broadcast.

Since there's no toolbar there, set the backdrop in the URL instead: …/overlay?room=…&panel=0.4.

Sharing your entire screen in Zoom shows the overlay to participants via the Electron window alone. Sharing a single window won't capture it — use the OBS virtual camera for that.

If your deck is already a web page (reveal.js etc.)

Paste the <div id="chat"> and <script type="module"> blocks from public/overlay.html straight into your deck, and load config.js and chat-core.js alongside them. Then you need no separate window at all.

Dealing with trolls

The send-side cooldown lives in the student's browser, so anyone who can open devtools can bypass it. The screens that render messages are the points you actually control, so that's where the defences are — applied identically on the projector overlay and on every student's page, from one shared chat-core.js.

Guard Behaviour
Flood limit 3 messages per name per 10 seconds; the rest never render
Duplicate filter The same person repeating the same line shows once
Blocked words Messages containing anything in config.jsBLOCK_WORDS aren't shown
Emoji validation The emoji lane renders at 52px, so only real pictographs get through
Clear screen 🗑 or Cmd/Ctrl+Alt+X removes what's currently on screen

Configuration

Everything lives in public/config.js.

Name Default Meaning
SUPABASE_URL Your Supabase project URL
SUPABASE_ANON_KEY The anon / public key
DEFAULT_ROOM lecture Room used when ?room= is absent
PUBLIC_BASE_URL '' Deployed URL, used only by qr.html to build the QR code
MESSAGE_TTL 14000 How long a message stays on screen (ms)
MAX_MESSAGES 12 Maximum lines visible at once
BLOCK_WORDS [] Words that suppress a message
NICKNAME_WORDS Korean word lists Two words are combined into a starter nickname (e.g. "조용한 수달") so students aren't all "익명". They can overwrite it. Replace the lists to change language

Overlay position and size come from the #chat rules in public/overlay.html; text size from body's font-size.

Layout

public/               ← the only thing that gets deployed
  index.html          student chat page (also the ?mini=1 popup)
  overlay.html        transparent overlay — receives only, never sends
  qr.html             join QR for the first slide
  chat-core.js        rules both screens share: name colours, flood/duplicate/word filters
  config.example.js   template → copy to config.js
  manifest.json       PWA manifest, for Add to Home Screen
  _headers            security headers (Cloudflare Pages · Netlify)
overlay-app/          ← runs on the presenter's laptop only, never deployed
  main.js             transparent always-on-top click-through window + 127.0.0.1 server
  preload.js          the only bridge between the toolbar and the app
start.cmd             one-click launcher (Windows) — ASCII only, cmd.exe parses it as OEM
start.command         one-click launcher (macOS)
tools/
  screenshots.mjs     regenerates the images in docs/ from the real pages
docs/                 README images

Regenerating the screenshots

The images above are captured from the actual pages, so they can't drift from reality. After a UI change:

npm i -D playwright && npx playwright install chromium
node tools/screenshots.mjs public docs

It serves public/, opens the pages with ?demo=1 (which fills in a fake conversation, so nobody else needs to be connected), and writes the PNGs.

What this design cannot do

These are traded away for simplicity. Know them before you walk into a lecture.

Limitation Why
Questions can't be revisited later Nothing is stored anywhere
No idea who wrote what No login; nicknames are free text
No pre-moderation You can only filter or clear after something appears
200 concurrent clients Supabase free-tier ceiling
A project can go to sleep between terms See below

Check the link the day before

Nothing here is a server you have to keep running. The static host has no process at all, and the Electron app only runs while you're presenting. But Supabase can pause a free project:

"We may pause applications on the Free Plan that exhibit low activity in a 7-day period to save on server resources." — Supabase docs

The docs don't define what counts as activity. So after a break longer than a week, the link still opens and looks completely normal while no message ever arrives — the worst possible failure, five minutes before class. Open the link once the day before. If it's paused you'll find out with time to un-pause it from the dashboard, and if it isn't, the visit itself is activity.

For a class of enrolled students this is generally fine. For a public talk, change the room name every time and only reveal the link in the room — that is effectively your only admission control.

If you ever add tables to this Supabase project, turn on RLS. Right now there are no tables, so an exposed anon key can do nothing. The moment a table exists, this chat page becomes that key's public distribution channel.

License

MIT — use it, fork it, change it. PRs welcome.

About

A transparent, click-through chat overlay for live lectures. Students join with a link; their messages float over your slides and fade out. No backend, no database - static files plus Supabase Realtime broadcast.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages