All projects
Featured case study

Lesson Whiteboard

A live whiteboard for online language lessons, built end to end across a Laravel API, Reverb WebSockets, the tutor app, and the student app.

Role
Full-stack engineer (sole developer across all three codebases)
System
EasyPeasyFluent / Edusogno live e-learning platform (20,000+ students)
A lesson page with live annotations broadcast to student devices

Results at a glance

  • Tutors annotate lesson material live; enrolled students see the marks appear on their own board, and get a recap after the lesson.
  • On a 1440×900 laptop in a real class (8 students, 12 materials, a 31-page deck), the material went from 7.2% of the screen to 31.5%: 4.4× the area.
  • Tutors log corrections anchored to the material and page they were made on, and can amend or withdraw them. Only the author can change them.
  • The backend suites cover the channel contract: tutor and student 200, outsider 403, soft-deleted enrolment 403, anonymous 401. 112 tests green at the pre-production pass.

Overview

The Lesson Whiteboard lets a tutor draw, highlight, and leave notes on lesson material during a live class. The student sees each mark arrive on their own screen, and after the lesson gets a recap of what the tutor marked for them and which corrections they received.

I built it end to end: the database schema, service, and endpoints in the Laravel API, WebSocket broadcasting over Laravel Reverb, the tutor's board, and the student's live mirror and recap.

The problem

Lessons were taught from PDFs and slide decks, and tutors had no way to mark them up where students could see. Corrections lived in tutors' heads or in chat. Once the lesson ended, the student had nothing to go back to.

The feature had to work across three codebases owned by different parts of the team, for a platform with 20,000+ active students. It also had to work in a real class, not a demo: eight students, a dozen materials, and a 31-page deck.

Architecture

API (api-v2, Laravel). Board annotations extend the existing annotations schema with an event scope next to the exercise scope. The two scopes are mutually exclusive by construction, and the new columns are hidden so the existing exercise endpoints return byte-identical responses. Rectangles use a packed, page-relative wire format shared with the student app's reading-exercise annotations.

Realtime (Laravel Reverb). Each write dispatches a ShouldBroadcastNow event on private-event.{id}.whiteboard: annotation created, updated, or deleted, and focus changed. The channel is authorised in one place. The assigned tutor and the enrolled students may subscribe. Admins and soft-deleted enrolments are refused.

Tutor app (tutor-v3, React + fabric.js). The board draws on a fabric.js canvas layered over the react-pdf page, with per-student annotations persisted through the API.

Student app (academy-v2, React). A live mirror of the board during class, and a post-lesson recap with filters for "On the page" and "Corrections".

Key decisions

A broadcast failure never fails the save

The write is the source of truth; the broadcast is a notification. The service dispatches events only after a successful write, and a broadcaster failure is logged but never turned into an error for the tutor. There's a test for each case: a refused write dispatches nothing, and a broken broadcaster doesn't fail the save.

Broadcast a pointer, not the text

When a tutor logs a correction, the channel carries whiteboard.corrections.changed as a pointer, never the correction text. Every client re-reads its own scoped view through the API. A student never receives another student's corrections, even though the whole class shares a channel.

Geometry belongs to the page

Annotations are stored as percentages of the page, not as screen pixels. That's why the same mark lands in the right place on a tutor's laptop and a student's phone. It also exposed a bug: marks were painted before the PDF had rendered, leaving the tutor's ink floating over blank white for seconds on a heavy page. The overlay now waits for react-pdf's onRenderSuccess, keyed by material and page, so a page turn re-arms it. A regression test holds the page in its unrendered state and asserts that nothing is drawn.

Degraded mode

If the WebSocket isn't live, the student board polls every 10 seconds, so the lesson keeps working. In recap mode the channel is never opened. The first version kept polling there too, re-reading the board all afternoon after the lesson ended. The interval now checks that the lesson is still live before it fires.

Giving the material the board back

At a real class on a 1440×900 laptop, the page the tutor taught from was 257×363 px: 7.2% of the screen. Fixed side rails took 48% of the width, and headers and padding took most of the height.

I moved the materials tray into a header dropdown and floated the corrections panel in the empty space beside a portrait page, so it costs no width. I also measured the stage from its actual container instead of a hard-coded 100vh - 140px. The same page became 537×760 px, 31.5% of the screen: 2.1× wider and 4.4× the area.

Bugs worth telling

  • Validation silently dropped a field. A note the tutor stretched came back at the default size on every page turn. The board did send meta.font_size, but Laravel's validated() returns only keys that have a rule, and meta.color was the only rule under meta. font_size now has its own rule, and a regression test checks the round trip through both the response and the database row.
  • A partial update overwrote the whole object. Changing a note's colour sent meta: { color } alone, and the API replaces meta wholesale, so the note snapped back to 20 px. The tutor app now sends the full meta.
  • A board that can't exist. The student app offered a Whiteboard button on writing corrections, mock tests, and meetings, where there is no tutor board. The API now returns 422 for those event types, the channel refuses the subscription, and the student app hides the button using the same list of event types the API uses.

Results

  • Live annotation and corrections in class, with a recap for students afterwards
  • 4.4× more screen area for the material in a real class
  • The channel contract, author-only edits, and event-type guards are covered by tests across all three codebases

What I took from it

  • Broadcast a pointer, not the payload. Each client re-reads its own scoped view, so the channel can never leak one student's data to another.
  • A save must never fail because the broadcast did. Real-time is a bonus on top of a durable write.
  • Store geometry relative to the page, and don't draw ink until the page has actually rendered.
  • When the socket drops, fall back to polling, and stop polling once the lesson is over.
Henry Iddirisu

Henry Iddirisu

AI product engineer · Accra, Ghana · Remote

More projects

LaravelPHPReact

IELTS Writing Corrections

Role: Full-stack engineer (API, tutor workspace, student view)

Replaced a Google-Docs correction flow with in-platform IELTS essay marking: tutors highlight and annotate spans and score four ba…

Next.jsReactTypeScript

Checkout Payments and Student Billing

Role: Full-stack engineer (payment step; billing API extensions; student and…

The payment step of a European e-learning checkout (Stripe, Klarna, Alma, PagoLight, SeQura), plus the billing API and student bil…