architecture · mediamesh · retro · realtime
SPACECOM: The Layer Everyone Mentions, Nobody's Toured
SPACECOM keeps showing up as someone else’s section. It gets a beat in a walkthrough of MediaMesh’s surfaces, filed under “multiplayer.” It gets a paragraph in a product overview, under “Multiuser.” It gets a passing line in an architecture retro about integration surfaces. Every one of those is a fragment of a larger thing none of them had room to fully explain.
Going in, that pattern looked like a red flag — four secondhand mentions and zero standalone coverage is exactly what a half-built feature looks like from the outside. So before writing a word of this, we went and checked.
The premise was wrong. SPACECOM turned out to have real design documentation behind it, covering the original decisions and phasing in detail, plus a full page on our docs site laying out the room model, auth, and client surfaces. “Nobody knows what this is” doesn’t hold up. The actual gap was narrower and more honest: we’d cited this layer four times and never once given it the standalone tour the material clearly supports.
One room per channel, one door in
Everything else in MediaMesh is a deliberately simple process model. A synced multiplayer queue needs somewhere to hold live per-room state and fan updates out to every connected client with low latency, so this layer runs as its own dedicated service rather than living inside the main application process — one isolated room instance per MediaMesh channel, created on demand rather than pre-provisioned. Keeping it a separate service also makes the whole layer optional: if it’s not configured, the rest of the app is unaffected.
The security boundary is the part worth calling out. Every audience connection receives the room’s full state snapshot, so anything secret can’t live inside that state at all. The operator passcode is checked only server-side against the connection’s own credentials — a connection is tagged operator or audience exactly once, at connect time, and never re-evaluated. State mutation is locked down even harder: a client has no path to change room state directly. Every mutation has to go through a role-checked message protocol — there’s no second door in.
Two sync paths, on purpose
Presence — name, color, which node an operator has selected — is small and low-frequency, so it rides the same persisted, broadcast-to-everyone path as the watch-party queue itself.
Cursor position deliberately does not. A live, high-frequency stream per connected operator would hit persistent storage for data nobody needs a history of, so cursor deltas go out through a separate fan-out-only broadcast with no state write and no persistence, throttled on the client to a fixed interval regardless of raw pointer-move rate — the same coalescing discipline already used elsewhere in the stack for high-frequency input.
On the Builder canvas, other operators’ cursor dots render in the same coordinate space the graph itself uses, so every viewer’s dot lands over the same node regardless of their own pan and zoom. Presence lives deliberately separate from the much larger canvas-editing state, so a fast-moving cursor stream doesn’t force unrelated parts of the UI to re-render on every mouse move.
Found live: the playback clock, twice
The queue’s playback position is server-authoritative — every client projects the live position forward from the server’s own clock rather than trusting its local one, the standard watch-party pattern. Meaningful drift forces a resync; small drift is left alone.
Two real bugs shipped in that logic anyway, both caught during actual multi-person test sessions, not code review:
An 87-hour seek. Pushing a new item into an empty queue didn’t reset the position clock in one specific edge case. A room left in a stale “playing” state from an earlier session meant a client would project its position forward from a timestamp days old — observed live as players seeking to roughly 87 hours into a video that had just started. Fixed by resetting the clock at the right moment in that transition.
A seek storm on embedded players. A second, independent bug hit specifically embedded third-party players, not direct file playback: the drift-correction logic kept forcing a correction on every tick with no cooldown, fighting an embed’s own real buffering behavior before a correction could even land. That produced a visible “stuck loading” loop, reproduced live with a real link. Fixed by adding a cooldown between forced corrections, giving one correction room to actually land before judging drift again.
Found live, again: three people actually using it together
Nothing in this section was caught by review. All of it came from an actual test session with three real operators connected at once.
- A protocol fix that had been sitting correctly in the working tree hadn’t actually been deployed yet — a stale build, not a design gap, caught the moment three people actually tried to connect together.
- The presence layer was wired to a live, freely-editable text field instead of an already-saved value — so every keystroke while renaming something fired a fresh request against a value that didn’t exist yet server-side, each one a failed call. Fixed by feeding presence the saved value instead.
- Every connection showed up as an indistinguishable random guest name with no way to change it. The fix added a small, explicitly-scoped operator roster as near-term scaffolding, kept deliberately separate from the anonymous-guest identity system that still backs the public watch-party audience.
The wall we ended up walking around entirely
An early deployment target for this layer hit a real quota wall on its hosting platform’s free tier — confirmed live — breaking every room until either the quota resets or the account moves to a paid plan. That’s a billing decision, not a code problem, and it’s since been resolved by leaving that hosting model behind rather than paying to keep it: SPACECOM now runs as a plain, self-hosted process alongside the rest of MediaMesh’s own services, with the same room-per-channel design and the same wire protocol clients already spoke to, just without a third party’s quota or billing terms sitting underneath it.
The move paid for itself well beyond just sidestepping a quota: memory usage for the same workload dropped from roughly 700 MB to roughly 20 MB. That’s not a rounding difference — it’s the gap between a general-purpose hosted runtime carrying its own overhead and a small, purpose-built process doing exactly the one job it needs to. Same reasoning as everywhere else in this stack: own the thing you depend on, rather than rent your way around its limits.
A second room, for a second purpose
Since this retro first went up, SPACECOM grew a second kind of room alongside the watch-party queue: per-channel audience chat, private one-to-one messaging between a viewer and whoever’s operating the channel, and a live audience presence count.
It’s a deliberately separate room type rather than an extension of the watch-party room, for a reason worth stating plainly: chat and DMs are a completely different broadcast shape than a shared playback queue, and folding one into the other would mean every chat message and every private message fanning out to everyone in the room, whether they’re party to it or not. Keeping them apart means a busy chat doesn’t touch queue-sync traffic at all, and vice versa.
The presence count follows the same discipline the rest of this layer already holds to: audience members are counted, never individually named or listed anywhere a client can see. Only the aggregate number is ever broadcast — nothing about who’s actually watching leaves the server as anything more specific than a total.
What’s not built yet
Polls and voting were always ranked behind queue sync and presence in the original design — a deliberate later phase. No poll state exists in the room model yet. That’s a genuinely deferred feature, not an abandoned one: when it lands, the design says it’ll follow the same shape as everything already built, riding the same per-channel connection with no new service required.
Verdict
Going in, four secondhand mentions and zero standalone coverage read like a feature that might be more aspiration than reality. What actually turned up: design documentation that phased itself honestly, a room model with a genuinely defensible security boundary, a deliberate split between two different sync paths chosen for the right reasons, three real bugs caught during actual multi-person sessions and fixed with root causes understood rather than papered over, a second room type added cleanly alongside the first without compromising either, and a hosting migration that turned a billing wall into a double win — no more quota, and a memory footprint over 95% smaller than before.
The gap this retro originally flagged — not yet on solid footing for real hosting — is closed. What’s here now is the version worth pointing to.