StreetWizard

// 09 // TECHNICAL DOCUMENTATION //

How The Engine Actually Works

Eight chapters, each written twice: once for a reader who does not write software, once for an engineer reading with the source open. Every number on this page was read out of the shipping code, and the file it came from is printed next to it.

READING LEVEL
// 01

Why Multiplayer GPS Is Hard

What breaks when everyone on a map can see everyone else?

Showing one car on a map is easy. Showing a hundred cars to each other, live, is not — and the reason is arithmetic, not programming skill.

If every driver has to be told where every other driver is, the amount of talking does not grow with the number of drivers. It grows with the number of PAIRS of drivers. Twelve drivers make 132 pairs. Two hundred drivers make 39 800. Add one more driver and you have not added one conversation, you have added two hundred.

That curve is what kills most attempts. It looks fine while you and three friends are testing it, and it falls over the first evening a real crowd shows up — which is exactly the evening you needed it to work.

StreetWizard does not try to route those messages more cleverly. It sends fewer of them. Everyone publishes to one shared channel, so the message count grows in a straight line instead of a curve — and then four separate mechanisms, described in the next chapters, cut what remains.

NAIVE — every rider tells every rider STREETWIZARD — one shared channel
 
[A]━━━━━━━━━[B] [A]──┐
┃ ╲ ╱ ┃ [B]──┤
┃ ╲ ╱ ┃ [C]──┼──▶ ( CHANNEL ) ──▶ all
┃ ╳ ┃ [D]──┘
┃ ╱ ╲ ┃
[C]━━━━━━━━━[D] messages = N
 
messages = N x (N-1) / 2
 
riders naive shared
──────────────────────────────────────────
12 132 12
60 1 770 60
200 39 800 200
Publish volume. The inbound side still scales with the crowd — the next three chapters are about shrinking it.
// 02

Heavy Identity Travels Once

Why does your avatar not weigh anything while you drive?

Two very different kinds of information travel between riders, and confusing them is the most common way to make a live map expensive.

Your name, your photo, your plate, the car you picked — that is who you are. It is comparatively large (a photo is a few thousand times bigger than a coordinate) and it almost never changes. Your position, heading and speed are the opposite: tiny, and different a moment later.

So they go down different roads. Identity is announced once when you join, and sits in a roster everyone already has. Position rides a separate, deliberately starved channel that carries nothing but the numbers that changed.

The practical effect: adding a photo to your profile costs the network one message, not one message per second forever.

PRESENCE — sent once, on join BROADCAST — the hot path
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ nickname │ │ lng, lat │
│ avatar ~5 kB │ │ heading, speed │
│ vehicle / level │ │ tens of bytes │
│ plate, colour │ └──────────────────────────────┘
└──────────────────────────────┘ 2/s → 1 per 4s
1x per session adaptive, ch. 03
 
▼ ▼
┌───────────────────────────────────────────────────────────┐
│ roster + positions ──▶ coalesced recompute / 350 ms │
└───────────────────────────────────────────────────────────┘
One channel, two modes. The avatar never touches the position stream.

// READ FROM SOURCE

  • TRANSPORTSupabase Realtime — presence + broadcast, one channelsrc/lib/fleet.ts
  • CHANNELwo-fleet-livesrc/lib/fleet.ts
  • AVATAR PATHpresence metadata only, cached in metaRefsrc/lib/fleet.ts
  • PRESENCE FALLBACKposition refresh ≤ 1 / 10 ssrc/lib/fleet.ts
  • RECOMPUTE COALESCE350 ms windowsrc/lib/fleet.ts
// 03

The Emission Ladder

Why does bandwidth per rider go DOWN as the crowd grows?

How often you need to broadcast your position depends entirely on who is looking, and that is a fact about eyesight, not about networks.

Rolling three cars deep with your crew, everyone is close and every small correction is visible — a slight drift, a lane change. There, position updates need to be frequent or the convoy looks like a slideshow.

In the middle of a two-hundred-car meet, the same car is a dot among dots. Nobody can tell whether that dot moved half a second ago or two seconds ago. Sending four times more data to render something no eye can resolve is pure waste.

So the rate follows the crowd, in four steps, automatically. Small convoy: twice a second. Two hundred cars and up: once every four seconds. Nobody notices the difference, because at that density there is no difference to notice.

There is a second gate that matters just as much: you also have to have actually MOVED. A car parked at a meet with its engine off broadcasts nothing at all, no matter how long it sits there.

fleet size min interval min distance the reasoning
──────────────────────────────────────────────────────────────────────────────
≤ 12 500 ms 5 m convoy — formation is visible
13 – 60 900 ms 10 m street — still reads smooth
61 – 200 2 500 ms 25 m crowd — prediction covers it
200 + 4 000 ms 40 m swarm — no eye can resolve it
 
BOTH gates must open:
 
moved ≥ min distance ─┐
├──▶ emit
elapsed ≥ min interval ─┘
 
parked car, engine off, GPS jittering 3 m → emits nothing
Read straight out of fleet.ts. The distance gate is what makes a static meet free.

// READ FROM SOURCE

  • ≤ 12 PEERS500 ms / 5 msrc/lib/fleet.ts
  • 13 – 60900 ms / 10 msrc/lib/fleet.ts
  • 61 – 2002 500 ms / 25 msrc/lib/fleet.ts
  • > 2004 000 ms / 40 msrc/lib/fleet.ts
  • GATEtime AND distance, both requiredsrc/lib/fleet.ts
// 04

The Map Is Smoother Than The Network

If a position arrives every 2.5 seconds, why does nothing stutter?

A position that arrives every two and a half seconds would, drawn naively, produce a car that jumps forward, freezes, jumps again. That is the classic multiplayer look, and it is unpleasant to watch for exactly as long as you watch it.

The fix does not involve sending more data. Each phone works it out locally. When a peer's position arrives, your device also knows which way that car is pointed and how fast it was going. From those it can carry the car forward on its own, continuously, while it waits for the next real fix.

So on your screen the car glides. Underneath, the network is delivering a fraction of the updates the motion implies.

There is a limit on how far ahead the guess is allowed to run — about a second and a half. Past that the guess stops, because a car that keeps sliding after its driver lost signal is worse than one that admits it stopped hearing.

And below walking pace the prediction is switched off entirely: at a red light, GPS noise looks exactly like slow movement, and a predicted car would drift out of its lane while standing still.

network ●·····························●························●
fix fix fix
t t+2.5s t+5s
 
screen ●───────────────────────────────●──────────────────────●
continuous — heading + speed carry it between fixes
 
guards
────────────────────────────────────────────────────────────────────
cap prediction stops after 1.4 s signal lost → marker halts
floor below 4 km/h derived red light → raw fix, no drift
blend velocity EMA 0.45 one bad fix cannot slew it
Same routine for the self marker and every peer — they degrade identically.

// READ FROM SOURCE

  • DR_MAX_S1.4 s prediction horizonsrc/components/map/camera.ts
  • DR_MIN_KMH4 km/h floor (derived, not announced)src/components/map/camera.ts
  • DR_BLEND0.45 velocity EMAsrc/components/map/camera.ts
  • APPLIED TOself marker and every peer, same routinesrc/components/map/NavExperience.tsx
// 05

Sixty Cars, Nearest First

What happens when a thousand riders are online?

Network cost and drawing cost are two different budgets, and solving one does nothing for the other. You can receive a thousand positions cheaply and still kill the phone trying to draw them.

Each car on the map is a real element on screen with a sprite, a name, a colour and a reputation badge. A thousand of those is a dead battery and a map at four frames a second — that is not a feature, it is a crash with extra steps.

So drawing is capped at the sixty nearest cars to you. Not the first sixty to connect, not a random sixty — the sixty closest, recomputed as you move.

That choice follows from what the map is for. The cars that matter are the ones you might actually see through the windshield. A rider four hundred kilometres away is a number in the online count, not a pin you need on your screen.

The online counter still shows the true total. The cap changes what is drawn, never what is reported — a thousand riders online reads as a thousand.

1 000 riders online
│
┌─────────┴─────────┐
▼ ▼
drawn: nearest 60 not drawn: 940
│ │
▼ ▼
HTML markers over still counted in
the GL canvas ONLINE = 1 000
 
┌───────────────────────────────────┐
│ · · │
│ · ● ● · │ ● drawn
│ · ● ▲ ● · │ ▲ you
│ · ● ● · │ · online, off-map
│ · · │
└───────────────────────────────────┘
sorted by squared distance, recomputed / 350 ms
The cap changes what is drawn, never what is reported.

// READ FROM SOURCE

  • MAX_DRAWN60 nearest peerssrc/lib/fleet.ts
  • SORTsquared planar distance, cos(lat) correctedsrc/lib/fleet.ts
  • ONLINE COUNTtrue total, computed before truncationsrc/lib/fleet.ts
  • MARKER TYPEHTML Marker over the GL canvassrc/components/map/NavExperience.tsx
// 06

The Community Odometer

Who is allowed to assert how far the community has driven?

The odometer is the one number on the map that belongs to everybody: every kilometre driven by every rider, ever, never reset.

A shared counter like that is only worth showing if it cannot be gamed, and the interesting engineering question is not how to add numbers quickly. It is who gets to assert one.

Each phone measures only its own distance, from its own GPS trail, filtering out the shake a parked phone produces so a car standing still never banks kilometres. It writes that to its own row and nothing else.

The community total is then computed on the server by adding up those rows. No phone ever sends the total, so no phone can set the total — the number on screen is a sum of individual claims, not a claim in itself.

Between two server reads the drums keep rolling: the client estimates forward from the live rate of riders currently on the road, then re-settles onto the real total when it arrives. It never rolls backward — a counter that visibly loses kilometres reads as broken even when it is being more honest.

DEVICE (untrusted) │ SERVER (privileged)
─────────────────────────────────┼──────────────────────────────────────
│
GPS trail │
│ haversine, jitter filtered │
▼ │
my km ──────── upsert ───────▶│ fleet_scores
│ ┌──────────────────────────────┐
no plausibility clamp yet │ │ rider A 412.7 km │
← the hardening gap │ │ rider B 88.1 km │
│ │ rider C 1 204.9 km │
│ └──────────────┬───────────────┘
│ ▼
│ SUM() · SECURITY DEFINER
│ │
community total ◀───────────────┼───── get_community_km()
│ │
▼ │
rolls forward between syncs, never backward
The trust boundary is the vertical line. The gap marked in magenta is real and open.

// READ FROM SOURCE

  • STORAGEfleet_scores — one row per ridersrc/lib/profile.ts
  • AGGREGATEget_community_km() — SECURITY DEFINER SUMsrc/lib/community.ts
  • CLIENT ROLLextrapolated between syncs, monotonesrc/lib/community.ts
  • OPEN GAPno server-side delta clamp on km writessrc/lib/profile.ts
// 07

One Song, One Convoy

How does a convoy hear the same track without a host?

The radio plays the same track to everyone tuned in, and a rider joining mid-song lands where the song already is — not at the beginning.

The naive way to build that is to elect one phone as the host and have it stream to the others. It works in a demo and fails on a road: the host drives into a tunnel and the music stops for everybody, so you need a re-election, and now every dropped connection is a renegotiation.

StreetWizard has no host. The server holds two facts — which track is playing, and the instant it started. A joining client reads those, works out how long the track has been running, and jumps its own player straight to that point.

Nothing is streamed between riders. The audio file comes from the same cache everyone else uses, and the only thing that had to be agreed on was a timestamp.

Riders will not be sample-accurate with each other — network jitter is real and pretending otherwise would be a lie. They land in the same place in the same song, which is what a convoy actually wants.

SERVER
track = "NIGHT DRIVE"
started_at = T0
│
┌──────────────┼──────────────┐
▼ ▼ ▼
join T0+12s join T0+47s join T0+92s
│ │ │
seek 12s seek 47s seek 92s
│ │ │
└──────────────┴──────────────┘
▼
same point in the same song
 
audio bytes: never on the realtime channel
static asset, long-lived cache, CDN edge
No host to elect, so nothing to renegotiate when a rider drops.
// 08

What WebRTC Would Actually Cost

The obvious upgrade is peer-to-peer. Is it?

Peer-to-peer sounds like the natural next step: skip the middleman, let the cars talk to each other. It is worth spelling out why that is the wrong first move here, because the intuition is reasonable and the arithmetic disagrees with it.

Start with the detail that makes this application different from the ones peer-to-peer was proven on. Those were two laptops on Wi-Fi, sitting still. This is a phone in a moving car, handing off from one cell tower to the next every few minutes — and each handoff can change its address and break the direct connection. An app built on peer connections would spend a drive re-establishing them instead of drawing the road.

What peer-to-peer saves is server bandwidth. But a position update is a handful of numbers — the entire live position stream is one of the cheapest things this application does. It would be trading real complexity for a saving that barely registers on a bill.

What it costs is worse. Direct connections between two phones on mobile networks often cannot be established at all, because carriers put their customers behind shared addresses. The standard fallback is a relay server — which means for those riders the data goes through a server anyway, and now you are paying for a relay you did not need before.

And the topology reintroduces exactly the problem this engine was built to avoid: sixty riders means each phone holding fifty-nine live connections, on a device already running a 3D map and a GPS. The quadratic cost does not disappear. It moves onto the phone, where there is far less headroom.

There IS a case for peer-to-peer here, and it is a different one: live VOICE between riders in a convoy. That is real-time media with strict latency requirements — the problem the technology was designed for. Positions are not.

FULL MESH — what it would take STAR — what runs today
 
[A]━━━━━[B] [A] [B]
┃ ╲ ╱ ┃ ╲ ╱
┃ ╳ ┃ ( ● )
┃ ╱ ╲ ┃ ╱ ╲
[C]━━━━━[D] [C] [D]
 
per client N-1 live sessions per client 1 session
at 60 peers 59 DTLS + SCTP at 60 peers 1
setup ~1s ICE, per pair setup once
CGNAT falls back to TURN relay n/a
TURN cost relays ALL media bandwidth tens of bytes
= MORE than today
 
AND THE ONE THAT DECIDES IT — the car is moving
tower handoff ──▶ address changes ──▶ ICE restart
every few minutes, every rider, all session long
 
the payload being fought over: lng, lat, heading, speed
2/s → 1 per 4s, already throttled
 
WHERE P2P WOULD EARN ITS KEEP
┌──────────────────────────────────────────────────────────┐
│ live VOICE between convoy riders — real-time media, │
│ strict latency, exactly what the technology is for. │
│ positions are not that problem. │
└──────────────────────────────────────────────────────────┘
The quadratic term does not vanish under P2P — it moves onto the phone.

// ENGINEERED BY

//SOFTWRE-ENGINEER//\\FULL-STACK//DEV\\
████████  ████████  ██    ██  ████████
██        ██        ██    ██  ██     ██
████████  ██████    ██    ██  ████████
     ██   ██        ██    ██  ██     ██
████████  ████████  ████████  ████████ .NET
S E B A S T I E N   -   G E R M A I N