← All posts
Real-time systems

Shared ink that is never peer-to-peer

Today

When a teacher draws on a shared coursebook page in a live lesson, the fast implementation broadcasts the stroke to every peer. That produces a classroom where what is on screen and what is saved can disagree, and where the student who joins late sees a blank page. The stroke goes to the database first and is broadcast by the server; only the ephemeral signals — viewport, pointer — travel the cheap path, and the receiver checks who sent them.

Dialogue — the operating system for a language school, spanning CEFR placement, the live classroom and the callback queue

The failure mode

Dialogue Dil Okulları has taught English and German in Fatih, İstanbul for 27 years. When lessons moved online, the school lost what a physical classroom gives away for free: who turned up, who left early, what was covered, and — most acutely for a language school — the shared page of the coursebook that a teacher points at.

Screen-sharing a PDF is not that. It is blurry, the teacher's marks do not survive the lesson, and the student gets no page of their own to work on.

The commercial consequence is the part that makes this worth engineering properly: a school cannot bill confidently for delivery it cannot evidence.

Why the obvious fix breaks

The obvious implementation of shared annotation is the fast one. The teacher draws, the stroke goes out over the realtime data channel, every peer renders it. Low latency, very little code.

It produces a classroom with two sources of truth for the same fact.

A stroke that reached four of five peers but failed to persist is on most screens and in no record. A stroke that persisted but lost its broadcast is in the record and on nobody's screen. Neither failure announces itself — the teacher keeps talking, and the disagreement is discovered later, by a student revising from a page that is missing the annotation the lesson was about.

The late joiner makes it concrete. A student who connects ten minutes in has missed every stroke that was broadcast before they arrived. Peer-to-peer has no answer to this except replaying history that nobody kept.

And a shared viewport invites a different problem: if any client can publish a page turn, any student can turn everyone else's page.

The rule that resolved it: anything worth replaying goes through the server; anything obsolete on arrival does not — and the receiver decides whom to trust.

The mechanism

Two rules make the book trustworthy rather than merely fast.

Shared ink is never peer-to-peer. A stroke is written to the database first and then broadcast by the server. "This ink is real" and "this ink is on your screen" cannot disagree, and a late joiner gets the same state as everyone else by loading rather than by replaying.

Ephemeral signals travel cheaply, and trust is checked at the receiver. Viewport, pan, zoom and the laser pointer are published lossy from the client — losing one is harmless because another arrives milliseconds later. A student's client honours those packets only from the presenter's identity, so the trust check happens at the receiver against the room's presenter rather than being assumed from the transport.

The lesson runs on LiveKit in a custom tabbed stage — Book, Screen, Camera — rather than an off-the-shelf video grid, because the book had to be a peer of the video rather than a screen-share hidden behind it.

The coursebook viewer — page 8 of 162 with its unit code, an annotation toolbar with pen colours and an eraser, a zoom control, and the four audio tracks printed on that page
The book as a first-class object rather than an uploaded file: page 8, unit 1A, annotation tools, and the four CD1 tracks printed on that exact page surfaced beneath it. The publisher's page content is pixelated here — it remains their property.

The edge cases that shaped it

The render DPI is chosen per page, not per book. The 162-page volume is imported by a pipeline that renders each PDF page to three WebP variants under content-addressed storage keys. The source turned out to be a hybrid: the first three units are native vector text, everything after them flat 144-ppi scans. A single global DPI would either have blurred half the book or fabricated resolution for the other half.

The audio programme is bound by reading the page. Each page's printed CD1 12 references are extracted and matched to files, so opening page 8 surfaces exactly the four tracks printed on page 8 — rather than a folder of audio the teacher has to search during the lesson.

Attendance fills itself in from signed webhooks. Signature-verified LiveKit webhooks record every join and leave. The system proposes an attendance state per student against a 50%-of-duration threshold, pre-filling left 22 minutes early before the teacher has typed anything. The teacher confirms with one click, then signs off a class-book entry that moves DRAFT → SUBMITTED → REVIEWED and locks against later edits. Evidence that requires a person to remember to record it is evidence you will not have.

Recording had to show the page, not the presenter. The lesson is captured through a custom room-composite egress template that renders the book alongside the camera strip, so the replay shows the page the class was actually on.

The CI gates catch what tests cannot see. Five Turkish localisation scanners check for missing diacritics, wrong I/İ casing, broken suffix agreement and text overflowing its control. A WCAG gate resolves the real Tailwind config and measures the contrast ratio an element actually paints, per theme, on a ratchet that fails on regression and on unclaimed progress. A third gate catches dead dark: classes — a dark-mode class naming a colour the palette does not define emits nothing, raises no error, and the dark theme silently never happens.

Limits, and what I would do differently

Writing before broadcasting costs latency, and I would pay it again. The stroke visibly lags the pen more than a peer-to-peer implementation would. For ink that a student revises from weeks later, a few tens of milliseconds is the right trade. For a laser pointer it would not be — which is exactly why the pointer is on the other path.

Presenter identity is a single value. The receiver check compares against the room's presenter, so co-teaching — two people who may both drive the page — is not expressible without changing that check into a set. Nothing about the design prevents it; it simply has not been needed yet.

I am deliberately not quoting adoption or revenue figures. The school had not switched the delivery modules on at the time of writing, so any percentage improvement would be invented. The honest claim is capability delivered, demonstrable end to end — placement exam to scored lead, lead to callback queue to payment, payment to scheduled lesson, lesson to webhook-derived attendance to a signed class-book entry, and a 162-page coursebook that synchronises across a live room — not usage measured.

If you are building shared state that people will rely on after the session ends, the question to settle first is which of your messages are facts and which are cursors. They do not deserve the same transport, and they do not deserve the same trust.

Frequently asked

Why not broadcast annotation strokes peer-to-peer?
Because it creates two sources of truth for the same fact. A stroke that reached four of five peers but failed to persist is on most screens and in no record; a stroke that persisted but lost its broadcast is in the record and on nobody's screen. Writing to the database first and broadcasting from the server means 'this ink is real' and 'this ink is on your screen' cannot disagree — and a student who joins ten minutes late loads the same state as everyone else.
What should travel over the cheap path then?
Ephemeral signals with no archival meaning: the presenter's viewport, pan and zoom, and the laser pointer. Losing one is harmless because another arrives milliseconds later, so they are sent lossy and cheaply from the client. The distinction is not importance, it is whether the signal is a fact worth replaying later or a cursor position that is obsolete on arrival.
How do you stop a student from driving everyone else's page?
The receiver checks the sender's identity against the room's presenter rather than trusting the transport. A student's client honours viewport and pointer packets only from the presenter identity, so a packet from anyone else is simply ignored. Doing the check at the receiver rather than assuming it upstream is what makes the cheap, lossy path safe to use.

Where this runs in production

Dialogue LMSThe operating system for a language school