Back to Spy NATS
Spy NATS Blog/ Live Classroom Q&A and Presence with NATS

Live Classroom Q&A and Presence with NATS

An education use case: a virtual classroom chat/Q&A subject per session, plus a lightweight presence pattern (who's currently online) built from core pub/sub and a KV heartbeat bucket.

4 min read
  • nats
  • education
  • chat
  • presence
  • tutorial

The scenario

A virtual classroom platform needs: a live chat/Q&A stream per class session, and a "who's currently here" roster the teacher can glance at — without a database round-trip on every keystroke.

Step 1 — one subject per class session

terminal 1
docker run -d --name nats-server -p 4222:4222 -p 8222:8222 nats:latest
nats context add local --server="nats://localhost:4222" --select
terminal 2 — everyone in the session subscribes
nats sub "class.101.chat"
terminal 3 — a student asks a question
nats pub class.101.chat '{"from":"student-a","text":"Is the quiz open-book?"}'
terminal 4 — the teacher answers, same subject
nats pub class.101.chat '{"from":"teacher","text":"Yes, notes are allowed."}'

Every message — question or answer — reaches everyone subscribed to class.101.chat, in order, the instant it's sent.

Step 2 — many simultaneous sessions, no cross-talk

Each session gets its own subject; a school running many classes at once needs zero extra infrastructure, since subjects are just strings:

terminal 2 — session 205, completely separate from session 101
nats sub "class.205.chat"
terminal 3
nats pub class.205.chat '{"from":"student-c","text":"Can we get an extension?"}'

The message never reaches the class.101.chat subscriber — subjects are isolated unless a subscription's wildcard deliberately spans them (e.g. class.*.chat for a school-wide moderation dashboard).

Step 3 — presence: "who's here right now"

Chat messages alone don't tell you who's silently listening. Add a lightweight heartbeat pattern: each client periodically writes its own presence key with a short TTL-like convention, and a KV bucket holds the current roster.

terminal 1 — needs JetStream for KV
docker rm -f nats-server
docker run -d --name nats-server -p 4222:4222 -p 8222:8222 -v nats-js-store:/data nats:latest -js -sd /data
terminal 2 — presence bucket for session 101
nats kv add CLASS_101_PRESENCE --ttl=30s
terminal 3 — each student's client 'checks in' periodically
nats kv put CLASS_101_PRESENCE student-a '{"joinedAt":"2026-07-24T09:00:00Z"}'
nats kv put CLASS_101_PRESENCE student-b '{"joinedAt":"2026-07-24T09:00:05Z"}'
terminal 4 — the teacher's dashboard reads the live roster
nats kv ls CLASS_101_PRESENCE

Because the bucket was created with --ttl=30s, any student key not refreshed within 30 seconds expires automatically — so a student who closes their laptop without saying goodbye silently drops off the roster, no explicit "leaving" message required.

Troubleshooting

SymptomLikely causeFix
Students in different sessions see each other's chatBoth published to the same literal subject (e.g. hardcoded class.chat)Always interpolate the actual session id into the subject, e.g. class.101.chat
Presence roster shows students who already leftClient's heartbeat loop stopped but bucket TTL hasn't elapsed yetExpected — TTL is an upper bound; shorten --ttl for faster staleness detection, but weigh it against kv put traffic volume
nats kv add ... --ttl=30s errors with "JetStream not enabled"Server started without -jsRecreate the container with -js -sd /data as shown in Step 3
Chat messages arrive out of order across two subscribersExpected in distributed delivery in general, but core NATS preserves per-publisher order to a single subscriberIf strict global ordering across all publishers matters, use a JetStream stream with a single consumer instead of plain pub/sub

Recap

ConceptTakeaway
class.<session-id>.chatOne subject per session keeps classes isolated with zero extra infra
class.*.chatA moderation/school-wide dashboard can watch every session at once via wildcard
KV bucket with --ttlCheap, self-cleaning presence roster — no explicit "user left" event needed

SpyNATS is an independent tool and is not affiliated with, sponsored by, or endorsed by Synadia Communications or the NATS.io project.