Loading portfolio
All Work

SyncLab

An interactive multiplayer networking lab where two players tag each other across a deliberately tiny arena while a live diagnostics panel makes client-side prediction, server reconciliation, snapshot interpolation, latency, jitter, and packet loss visible and toggleable in real time.

Design + Build · SoloVisit live site
Server tick rate
0 Hz
Snapshot rate
0 Hz
Interpolation delay
0ms
Netcode systems
0
02(Context)

SyncLab exists to make a hard-to-explain systems problem visible and feel-able: how can a client respond instantly when the server still owns the truth? It began with the smallest multiplayer interaction that could carry the point, two circles in a bounded arena with tag transferring on touch, deliberately forgettable rules so the networking behavior is the entire point. It's built for engineers, or engineering-curious learners, who want to feel prediction, reconciliation, and interpolation happen under real, adjustable network conditions instead of reading about them abstractly.

03(Design)

The control surface is split into two tiers on purpose: four one-click presets (Clean, 200ms, Jitter, Lossy) for instantly reproducing a known network scenario, plus continuous sliders for round-trip latency, jitter, and packet loss for exploring in between. A first-time visitor gets the “aha” in one click; anyone curious gets a way to dial conditions precisely. The presets and sliders share the same state, so nudging one updates the other’s position live.

The arena is a role="application" region that has to be explicitly focused, by click or tab, before WASD or arrow input is captured, and it clears every pressed key on blur. That’s a small, deliberate robustness decision: it stops a stray keypress from moving your avatar after you’ve tabbed away, and keeps the keyboard contract explicit (aria-label="SyncLab arena. Focus and use WASD or arrow keys to move.") rather than silently hijacking global key events.

The optional ?debug=true overlay is progressive disclosure: by default the game just looks like a smooth multiplayer demo, but adding the query param reveals four simultaneous position markers (authoritative, predicted, rendered, and remote snapshot), so a visitor can watch reconciliation actually happen, markers separating under packet loss and snapping back together, instead of taking the netcode’s correctness on faith. An aria-live="polite" flow feed streams the six most recent input, snapshot, ack, reconcile, and drop events, color-coded by type, so state changes are visible to assistive tech, not just to sighted users watching the canvas.

04(Engineering)

SyncLab is a pnpm-workspace monorepo: a Next.js 16 / React 19 client, a standalone Node.js server on raw ws with no game engine, database, or auth, and a shared package of pure functions (movement, reconciliation, interpolation, protocol guards) imported by both sides so client prediction and server simulation can never drift into two implementations. The server runs a 30Hz fixed-timestep accumulator loop, with capped catch-up steps guarding against event-loop stalls, and broadcasts authoritative snapshots at 20Hz on its own accumulator. A network-simulator layer sits in front of the WebSocket in both directions and deterministically delays or drops whole messages per room. It works at the message layer, not the packet layer, which buys reproducible, deterministic scenarios at the price of never reproducing partial-packet behaviour the way real UDP or TCP would. The core mechanism is the reconcile step: on each snapshot, the client restores the server-acknowledged position, discards inputs at or below that sequence number, and replays only the unacknowledged ones through the same shared movement function: deterministic correction, not interpolated guessing.

Input
30Hz input sampler
WASD/arrows → sequenced commands
→
→
Render
20Hz snapshots → rAF canvas
Remote players smoothed 100ms behind
stack   Next.js 16 · React 19 · TypeScript · Tailwind 4 · Radix/shadcn UI · Node.js + ws · pnpm workspaces · Vitest
05(Why end-to-end)
Because I owned both the client-side prediction and the authoritative server simulation, I could share one applyMovement function between them, so prediction, authority, and reconciliation replay all move the same way instead of the drift two independently-maintained prediction and authority layers would have caused.
The case for one person, end to end
06(Outcome)

Public and live at synclab.javiertpadilla.com, the Next.js client on Vercel with the ws server on Railway behind it, exactly the split vercel.json and railway.json were built for. The full loop ships: the two-player arena, all three netcode systems independently toggleable, the four network presets plus continuous latency/jitter/loss sliders, and the optional ?debug=true reconciliation overlay.

0
Unit tests, shared netcode logic
0
Network presets
07(What I'd do differently)

The README is honest about SyncLab's edges rather than hiding them: it states plainly that the simulator delays and drops application messages rather than emulating real UDP/TCP packet behavior, that the room process is single-instance and in-memory with no cross-region coordination, and that there's no lag compensation because the only interaction is proximity tagging. Test coverage stops at the shared math and one server validation test; game-client.ts (520 lines) and the network simulator have none. There's a small doc/reality gap too: the README says to copy apps/web/.env.example to .env.local, but no .env.example exists in apps/web, so I had to hand-write the env file to run it. The decision I'd keep regardless is the shared-package movement function: it's the one thing preventing client and server from silently diverging, and it's the reason the reconciliation demo is trustworthy instead of just plausible-looking.

Next project
RLS Generator ↗
Dev tool · Visual builder / Next.js · TypeScript · Tailwind
← Back to selected work