Skip to content

Acelab · Material Hub

Keeping every open screen current as shared data changes

Collaborators on a shared schedule see each other’s edits and selections live, discuss cells in threaded comments, and get notifications in one center that catches up after a dropped connection. Built with my team and shipped between 2024 and 2026.

Problem
When many people have the same data open, every screen has to show their changes as they happen and recover the ones it missed.
My part
With my team I built presence and live cells; I built the first notification center, finished a teammate’s threaded comments, and built the real-time event for a teammate’s notification engine, taking its frontend to release.
Result
Collaborators on a schedule see each other’s edits and selected cells live, discuss cells in threaded comments, and get notifications in one center; all shipped between 2024 and 2026, with the old center removed.

The problem

When several people share data, every screen has to keep up: a value someone just typed, who is looking where, a reply, a request awaiting an answer. Each change must reach every tab without a reload, survive arriving twice, and be recovered after a laptop sleeps or a connection drops.

At Acelab that data is the product schedule in Material Hub, a materials platform for architects, owners and manufacturers. A schedule is a spreadsheet-style list of the products and finishes in a drawing set; Acelab markets one kind as Finish Schedules (opens in a new tab). Collaborators comment on its cells and get notified when someone needs them.

How it works

All four features share one transport: the server publishes each change on a channel per schedule, project, user or company. What differs is how an open client applies it.

How events reach an open client, and how it catches upThe server publishes an event for each change on a channel: per schedule, per project, per user or per company. The client applies each kind differently. Cell edits patch one cell and selections update one collaborator’s entry, both found by row and column id. Presence events track who joins and leaves, and newcomers receive the current selections. A comment thread event makes the client refetch the threads for the view it shows. Notification events insert, update or remove items by id and carry the server’s counts. Highlighted: after a reconnect, or when a long-hidden tab comes back, the client resyncs and replaces its notification list and counts with the server’s.Serverpublishes an event for each changeChannelsper schedule, project, user or companyCells and selectionspatch one cell or selection by row and column idPresencejoin and leave; newcomers get selectionsComment threadsrefetch the threads for the open viewNotificationsupsert or remove by id; server countsReconnector a long-hidden tab comes backSCHEDULESCHEDULEPROJECTUSER, COMPANYresync: replace the listand counts from the server
Four kinds of events and how an open client applies each, with the notification catch-up path highlighted.

Patch in place. With my team I built live cell updates and presence: who else has the schedule open and which cell each person has selected. I created the real-time store and presence composable underneath. A cell event carries the new value, and the client patches that one cell, found by row and column id, with no refetch. Selections go out after a half-second pause, so moving quickly across a row sends one message, not one per cell, and a cell outlines itself in the color of whoever is on it.

Refetch. I took threaded cell comments over from a teammate and carried them to general release, with fixes to editing, replies and mentions, a reworked product-link preview, and .NET changes that route notifications to the right thread. I attached each thread to its cell rather than the view it was written in, so it shows in any view containing its row and column. The server works out which threads a view shows, so a thread event tells other open schedules in the project to refetch theirs.

Merge by id, then resync. In 2025 I built the first in-app notification center: the bell and panel, grouping, read states, filters, actions taken from a notification, and real-time updates. Events insert, update or remove entries by id, so a duplicate changes nothing. On a teammate’s new notification engine, I proposed and built its single real-time event on server and client, added server support for actions that ask for input, markdown messages and links into comment threads, and took its frontend through review to release. Its client, which the teammate started, resyncs after a reconnect or when a tab hidden for over five minutes returns.

Decisions

Decision

Send one notification event that says what to do with it.

Instead of
Five event types, with the unread count in its own event, as the engine’s first version had.
Why
With the count sent separately, the badge could lag the notification it counted, and only a new notification could raise a toast. I proposed one event carrying the notification, the current counts and a delivery instruction (toast, remove or resync), so the badge changes with the notification it counts and the server decides behavior in data, not by event name.
Cost
Every write path on the server must assemble the full event, counts included, before publishing. Company-wide broadcasts can’t carry anyone’s own counts, so they only tell clients to resync.

Decision

After any gap, replace local notification state with the server’s.

Instead of
Merging in what changed since the last sync, timed by the client’s clock, as the new frontend’s first version did.
Why
A merge never removes anything, so a notification dismissed or canceled while a client was offline stayed on screen, and the client’s clock could drift from the server’s timestamps. I made each resync fetch the current window and replace the store. Marking a notification read applies the counts in the server’s response instead of waiting on the socket.
Cost
Each resync downloads the whole recent window, not a delta. A first load also used to resync twice; now only a real reconnect triggers another.

What I’d change

My first notification center trusted the stream. It loaded its list once and let events keep it current, and the badge added or subtracted one per event, so a dropped connection or a missed event left both wrong until a reload. The engine’s client fixes both; next time I’d build the catch-up path first.

In the 2024 presence store, state outlived the page until a hotfix cleared it, and the store rebuilt its map of who is on which cell, scanning each selected row, whenever rows or selections changed. I’d now tie such state to the subscription and build those maps from lookups.

Outcome

Collaborators now see each other’s edits and selections live, discuss cells in threaded comments, and get notifications in one center that catches up after a gap. Live cells and presence merged in August 2024 and the first notification center in July 2025; comments reached general release in June 2026, and since July 2026 the engine-backed center is the only one. What transfers: apply data events by id, and after any gap let the server’s answer replace local state.

What’s left out

Acelab’s code and data are private, so the diagrams are redrawn and any interactive demo is a rebuild with made-up data. Names of colleagues, customers and internal tools are left out on purpose. I’m glad to go deeper on any of it in an interview.