Skip to content

Vaynerov Technologies

We don't just develop — we conjure every line of code & pixel.

All articlesBuild stories

A Museum with Other People in It

Version one was a museum for one person. Version two is the same building with other people walking through it: four characters to get in, up to eight visitors, voices that fade as they wander off — and not one byte of any of it kept on a server.

Edward AmirainFounder, Vaynerov Technologies
Published 13 min read
On this page
  1. A room is a channel, not a record
  2. A relay with no authority
  3. Eight people in wool and linen
  4. Voices that fade when you walk away
  5. Visiting without being written to
  6. The seam version one left open
  7. The building itself got better
  8. What shipped quietly

Six weeks ago I wrote up a museum for anything with a URL — the guarded fetch, the parse cascade, a floorplan that never moves. It worked, and it was a building for exactly one person. You could paste a link, hang a thing, wander your own halls, and then the only thing to do with what you had built was describe it to somebody.

So version two is the same museum with other people in it. You open a room, read out four characters, and up to seven more visitors walk into your building — as figures you can see across a gallery, with voices that get quieter as they drift into the next room. The building itself improved while I was in there: three kinds of gallery instead of one, sculpture on plinths, a sky that changes by the day, a guided tour that is finally a real camera controller.

None of it is stored anywhere. That constraint did more design work than any feature did, so it goes first.

8
visitors in a room
0
bytes stored server-side
246
interface strings ×6 locales
303
unit tests

Reliquary v2's operating shape. The second number is the one v1 shipped with — keeping it through multiplayer is the point of most of what follows.

A room is a channel, not a record

The obvious way to build visiting is a rooms table: a row per room, a roster, history for the chat. It is also the point at which your museum never leaves your browser quietly becomes …except the parts we now keep. So there is no room record anywhere. A room is a channel topic — vt:reliquary:v1:<CODE> — on a realtime broadcast relay, and the host's browser is the room. It lives while the host's presence key sits on the channel; when the host closes the tab it is over.

Having no record has a consequence I did not anticipate: there is no way to ask whether a room exists. A topic nobody is on subscribes exactly as happily as a busy one. So presence and a deadline answer the question — a joining guest waits six seconds for a host key to appear, and that timeout is the definition of "no such room." Before a host takes a code, the same absence makes it sit on the topic for 900 ms to see whether anyone already lives there.

The code is four characters, shown as REL-7K2M. The alphabet has thirty symbols: no 0 or O, no 1, I or L, no U — the first two pairs are the classic read-aloud confusions, and U is gone so no draw can spell something a visitor would rather not say out loud. Thirty to the fourth is 810,000 codes, drawn from crypto.getRandomValues. That is private-by-obscurity and says so: guessing one is the only door into somebody else's museum, so the host holds a close-the-room control and every arriving byte is re-validated regardless.

A relay with no authority

The credential that gets a browser onto that channel is public by design — the marketing pages already ship it to every visitor. The honest description of the transport is a dumb pipe that anyone holding four characters can send anything into, and everything downstream is built on that assumption.

Every message is re-validated on arrival, by type and by bound, in one function that is the whole trust boundary of the feature. Exhibits arriving from a peer go back through sanitizeExhibit — the same function the visitor's own storage is filtered by, imported and never copied so it cannot drift. A URL refused off disk is refused off the wire. The sender id is a claim rather than a credential, and there is deliberately no signing layer, unlike the 4D chess wire where a forged move could steal a game. Here there is nothing to steal.

The sender is never the sanitizer. A guest's mount is a request; the host re-sanitizes it, applies it through its own store, and rebroadcasts what actually landed.

What did need defending is amplification. A thirty-byte hello that buys a sixty-exhibit resync is a lever pointed at the room, so every message whose answer costs more than the message carries a per-sender floor: two seconds between resyncs, 400 ms between chat lines, three seconds between mount requests. Typing is slower than 400 ms; a loop is not. The sync is chunked at 48 KB or 24 items, whichever comes first — and an empty collection still emits one chunk carrying an empty list, because a guest that receives nothing cannot tell "the host owns nothing" from "the sync is still coming."

The channel carries introductions and nothing with authority. Audio never touches it: the relay hands two browsers each other's opaque connection blobs, and the media goes direct.

Position is the one message that repeats — ten a second, per visitor, for as long as the room is open — so it is quantized to centimetres and milliradians. That is not only compression: the quantizer is also the idle detector, since two poses that quantize to the same integers are the same pose, and standing still therefore stops the broadcast with no separate test.

Eight people in wool and linen

v1's spec had two lines at the top: no new npm dependencies, no binary assets. The second decided what a visitor is. No uploads, no downloaded characters — a visitor is four small integers and a name, and the whole customizer's output fits in a presence payload.

Eight palettes, named for cloth rather than for colour: slate, camel, plum, moss, rust, ink, linen, verdigris. The gallery's vocabulary is walnut, limewash and brass under a low sun, and every outfit sits inside that range rather than fighting it. Exactly one palette reaches toward teal, two steps down in chroma — a visitor in a teal-ish coat, not a light source walking through the room. Teal here is still a scalpel.

The four head shapes are one curve. A superellipse — |y/ry|^n + |x/rx|^n = 1 — is an ellipse at n = 2 and approaches a rectangle as n grows; round, oval, square and crest are four parameter sets of it, the square head at 3.6, a soft cube of revolution rather than a box with corners. One function emits the silhouette, the 3D figure lathes it, the dressing room's flat preview mirrors it into a path, so the picker cannot promise a head the figure does not keep. Height is a tight band, 0.9× to 1.1× of a 1.70 m figure.

The decision I expected to argue with myself about was collision. Peers are ghosts: no avatar contributes a collider and the walk controller is never told they exist. What we draw is an estimate — a spring-smoothed guess at where somebody was a couple of hundred milliseconds ago — and being shoved by an estimate is worse than walking through a person who is really there. And a doorway is 2.3 m wide: one idle visitor standing in it, solid, seals a wing of a stranger's museum with no way to ask them to move. Overlapping is a two-second embarrassment; being trapped is a broken visit.

Voices that fade when you walk away

Text chat was a morning. Voice is what turns a shared page into a shared room. Audio flows browser to browser over direct peer connections; the channel only hands two visitors each other's opaque session blobs. That is what fixes the cap at eight, host included: a full mesh of eight is 28 connections and seven upstreams per person, about what a laptop uplink and a phone's encoder survive. Raising it would mean a media server, not a bigger constant.

Both ends of a pair learn about each other from the same presence event, in the same instant, so both try to open the conversation and both deadlock — the classic glare. The fix costs one comparison: the peer whose id sorts first offers, the other waits. Peer ids are 32 hex characters, so the ordering is total and identical on both sides. Voice is opt-in per visitor, so an answerer that has heard nothing for 2.5 seconds promotes itself and offers anyway.

Then the part you actually hear. A voice is at full gain inside two metres — conversation distance — and falls on plain inverse distance after that. Physical inverse never silences anyone, though: at fourteen metres a voice is still around 14 % gain, and seven of those under the room tone is a murmur that sounds like a fault rather than like a museum. So the curve is windowed to exactly zero over its last stretch by a smoothstep, flat at both ends so nobody clicks off as they walk away. The rooms here are about ten metres across, which puts the fade between the far end of this room and two rooms away.

Both axes linear from zero. The dashed line is the inverse law alone, still audible at the far edge; the shipped curve is that law times a window, which is what makes distance mean something in a building this size.

Panning is the sideways component of the bearing, capped at ±0.8, and it collapses toward centre below a metre so two visitors passing through each other do not whip left-to-right in your ears.

Voice runs its own audio context rather than borrowing the games engine's, which suspends itself the moment the document is hidden: correct for footsteps, wrong for a conversation. It still obeys the same mute, because somebody who muted the museum meant make it quiet, not make the chimes quiet. Push-to-talk sits on V and gates the microphone track rather than the connection.

The residual, named rather than buried: there is no relay server yet. Two browsers whose networks refuse to meet directly never connect. That pair is marked unavailable in the visitor list and keeps text chat — worse than working voice, much better than a spinner that never resolves.

Visiting without being written to

A guest is looking at somebody else's collection, and the promise from the launch note is that your museum is one key in your own browser. So a visit renders an entirely separate world from the one your key holds, and your storage is never written while you are out. The swap rides a veil in both directions, and it veils immediately, before the chunked sync has landed — the alternative is leaving a guest standing in their own gallery for a beat, wondering whose paintings those are.

Read-only turned out to be a series of small judgements rather than one switch. The manage drawer keeps its list and its walk-to-it, but remove and clear are gone. The paste bar stays and becomes a request: the parse still happens in the guest's browser, the sanitized result travels, the host sanitizes again and answers with a verdict that arrives as a toast. With mounting off the bar is disabled and says why rather than vanishing, because a missing control is indistinguishable from a broken page.

The seam version one left open

The launch note ended on a promise: a guided tour, "sketched behind the same controller seam the walk mode uses." For a while that tour was what such things usually become — a chain of one-shot camera tweens driven from inside the render loop, which meant the camera had two owners depending on the minute.

There is a second implementation now. The rail is a precomputed stop list: every work in hanging order, each at the head-on viewing pose the reveal walk already uses. Between stops the camera flies the doorway-waypoint polyline rather than lerping through a wall, dwells 3.6 seconds, and wraps at the end. Drag-look while riding adds a bounded offset on top of the rail's own facing — it decays back to centre while travelling, so the next artwork always arrives centred, and holds while dwelling. Touching the movement keys is the real takeover, and it hands you back to walking at the current pose. The better consequence of a controller rather than an animation: opening an artwork now pauses the tour instead of ending it.

The minimap comes off the same layout module the building does — entrance at the top, galleries running down the panel in the order you walk them. Click a room or a dot and the existing walk-or-veil machinery takes you there. Other visitors appear as dots in their own accent colours; the only teal on the panel is the wedge showing which way you face.

The building itself got better

v1 had one gallery recipe: a long hall with art alternating down two plain walls. It is a good room, and by exhibit thirty it is the only room you have ever been in. There are three archetypes now, cycled by room index — the hall, a salon (a broader state room with corner pilasters under a ceiling oculus), and an alcove gallery where shallow piers step out of each side wall between hangs, so every pair of works sits in its own bay.

The invariant that made this delicate is the one the whole building rests on: appending exhibit N+1 must never move exhibits 1 through N. So everything an archetype changes is a pure function of the room index, and room 0 is deliberately the hall with the original numbers, so every museum built in June reproduces exactly the geometry it had. Image-less exhibits used to hang as a plate on the wall; they now stand as an engraved stele on a plinth, at the identical slot position.

And the sky moved. Three moods — golden hour, a pale morning, a blue-hour dusk — are picked from a ten-slot weighted wheel by hashing the local calendar date. Golden hour keeps five of the ten because it is the house face; you get one weather all day, a different one tomorrow, and two people in the same timezone see the same museum. Each mood carries the whole lighting contract, not just the sky: dusk raises the plaster's warm self-glow rather than lowering it, because the wall above a doorway is reached by neither the low sun nor the art spots.

What shipped quietly

A WebGL probe now runs before the canvas mounts, and a genuine absence gets a localized poster instead of a blank rectangle — genuine absence only, never device class: a phone with working WebGL keeps the whole walkable museum. Context loss is wired to a recovery state and a clean remount.

The artworks are meshes, so there is nothing to Tab to. The pattern that fits a drag-look canvas is a cursor over the hanging order: [ and ] step through the works, the camera walks or veil-teleports to each one, and each is announced politely. Enter inspects — and with no cursor set it picks the artwork nearest to where you are standing.

And the museum has tests now: 303 across 11 files, up from none. That is architecture more than virtue: the protocol, the room codes, the voice math, the layout and the rail are pure modules with no React, no three.js and no network in them, which is why why is that person's voice on the wrong side is a test rather than an afternoon in a browser. The interface went from 87 strings to 246, all six locales, parity checked.

v1 — Junev2 — today
Visitors1up to 8, host included
Voiceproximity mesh, direct peer-to-peer
Guided toura chain of camera tweensa rail controller; focus pauses it
Gallery roomsone hall recipe3 archetypes, cycled by room index
Skygolden hour3 moods, one per calendar day
Image-less lotsa plate on the walla stele on a plinth
Finding your waywalk, or the manage listfloor-plan minimap with peer dots
Interface strings87 ×6 locales246 ×6 locales
Unit tests0303 across 11 files
Stored server-sidenothingstill nothing

The last row is the thesis. Everything above it is what it takes to keep the last row true once there is more than one person in the building.

The privacy model in version one was too simple to get wrong: no database, so nothing to lose. Multiplayer is normally where that sentence gets amended — you need a room somewhere, a roster somewhere, a little history so the chat scrolls. It turns out you do not. You need a channel, a code short enough to say out loud, and the discipline to re-validate every byte that arrives on it, because a relay with no authority is only safe if nothing downstream trusts it.

What I did not expect was how much better the building got as a side effect. Once a room can hold other people you notice that every gallery looks the same, that the sky never changes, that the text-only lots are apologising for themselves against a wall. Nobody minds that alone. You only see it when somebody else is standing in it.