Background
The LMS web app runs live tests inside a video classroom (MeetMate) popup window. The two apps have different repositories and different release cycles, and the only way they talk to each other is the browser’s cross-window postMessage. In late June a request came in — “show the score report as soon as the student submits in the classroom” — so I first wired up a flow in which the classroom window asks the LMS window for the score report PDF, and the LMS generates the PDF and sends it back.
The problem was how messages were validated. At first the reply target was a wildcard, and the validation added later still depended on an allowlist of domains. On July 6, one production address was missing from that list, and the result was an outage in which score reports loaded endlessly without any error.
Approach
On the day of the outage I restored service by adding the missing address, and the next day I redesigned validation to decide based on the window’s identity rather than its domain. The decision has three layers. ① The LMS remembers the classroom windows it opened itself in a WeakSet and checks whether the message’s source is the same window. This is independent of domain and survives redirects along the way. ② Reloading the LMS wipes out ①’s memory, so it also checks whether the sending window’s opener is this window. That reference is guaranteed by the browser and cannot be spoofed. ③ The existing allowlist stays as a last safety net.
During review I also considered an “allow anything under the same parent domain” approach, but if public-suffix detection (things like co.uk) is incomplete, unrelated domains can slip through, so I didn’t use it. Every origin that used to pass stays in ③, which makes the new decision a strict superset of the old one. By construction, the change cannot block an existing environment.
The allowlist was a structure that “silently breaks if you miss one.” After switching to identity-based checks, there is no code to change when environments are added.
On the reply side, I pinned targetOrigin from a wildcard to the origin of the window that sent the request. I also routed the receivers for the “submission complete” notification and the “go to score report” message, which until then had accepted messages without validation, through the same entry point. I consolidated the messages into 5 types — submission complete, open score report, and PDF request / data / error — and on receiving submission complete, the list refreshes via a throttled refetch so it switches to “completed” without a page refresh.
In late July, server logs showed an incident where the same answers were saved twice, 0.06 seconds apart. I added a per-test single-flight guard to the answer submission function: if a submission is already in progress, it doesn’t send a new request and returns the same Promise instead. It releases as soon as the call finishes, so legitimate resubmission isn’t blocked. On the submit buttons of the three test-taking screens — OMR, survey, and attachment — I added a separate re-entry guard based on a synchronous ref to block double taps as well. I aligned it with the same rules on the classroom app side.
Finally, I documented the limits of the coupled-window design itself: it fails if the LMS window is closed, it only works when both apps’ deployed versions match, and, as an emergency measure, the PDF generator is duplicated across both repositories. I turned a proposal — have the server pre-generate each student’s PDF when statistics are processed after the deadline — into a discussion document for the backend team, covering the required data, permissions, and failure cases.
Results
- Cross-window message validation: one layer of domain allowlist → three identity-based layers. Zero code changes when adding a new deployment environment; every previously accepted origin preserved.
- Score report PDF reply target: wildcard → the requesting window’s origin. The 2 receivers that had no validation now follow the same policy.
- Duplicate answer submissions (0.06s apart) → per-test single-flight + re-entry guards on all 3 submission screens.
- Submitting in the classroom → the LMS list reflects completion in real time, and the score report PDF can be viewed right from the classroom popup.
- Wrote up the server-side score report PDF pre-generation proposal as a supporting document and handed it to the backend team.
What I Learned
Validating postMessage by where it came from (domain) meant being set up to break every time an environment was added. Switching to who sent it (is it a window I opened) made the check stricter while actually reducing the operational burden. I also confirmed that if you design the new check as a superset of the old one, a change that tightens security doesn’t introduce regressions.
One more thing: the emergency fix and the structural fix had to be kept separate. On the day of the outage I stopped it with one line, the next day I eliminated the whole class of cause, and I handed off the remaining structural coupling as a document. Keeping those three steps apart let me clean things up without stopping production.