Luis Juárez
← All projects

PUZZLELOVE

A real-time multiplayer jigsaw puzzle — upload a photo, it is cut into classic tabbed pieces, and everyone in the room assembles it together with live cursors, piece locking and snapping sounds. The game is the visible part; the rest of it is the production platform and data stack around it, all running on one 8 GB VPS that I provision, deploy, monitor and back up myself.

Play it →The game itselfAnalytics dashboard →Apache Superset, public and read-onlyService status →Uptime KumaSource →Monorepo, CI/CD and infrastructure as code

HOW IT FITS TOGETHER

PlayersTraefikHTTPS / WebSocketPortfoliothis site, at /Game serverNode + Socket.IO, at /puzzlelove/Postgresstate + eventsGarage (S3)images + lakePySpark + Deltanightly, 04:30WarehousePostgres star schemaSupersetpublic/adminprivateCloudflare R2age-encrypted, daily

WHAT WAS BUILT

Software

TypeScript monorepo: React 19 + Konva board, Node/Express 5 + Socket.IO authoritative game server, a deterministic puzzle engine shared by client and server, Prisma over Postgres.

Cloud & infrastructure

Hardened Ubuntu VPS provisioned with Ansible, Docker Compose, Traefik with automatic Let's Encrypt, Garage for S3-compatible object storage, layered firewalls, and cgroup resource tiers so no app can starve the host.

CI/CD

GitHub Actions: tests, then five images to GHCR, then an SSH deploy that runs migrations, waits on a health check and rolls itself back automatically when it fails.

Data engineering

Gameplay events land in an append-only table through a non-blocking sink, are extracted incrementally into Delta Lake bronze/silver/gold with PySpark, pass data-quality gates, and are published into a Postgres star schema with a transactional swap.

Operations

Daily age-encrypted backups to Cloudflare R2 with tested restores, Uptime Kuma plus an external probe, and Discord alerting.

Testing

Automated tests across the engine, the server (with real Socket.IO clients), the simulator and the PySpark transforms, plus an Ansible idempotency test and lint in CI.

THE DECISIONS I CARE ABOUT

The server is the referee

A puzzle is defined by an image, rows, columns and a seed. Client and server derive identical piece shapes from that seed, so only positions ever travel over the network. The server owns the locks, batches movement every 50 ms, decides which groups snap and detects completion — a client cannot assert that it solved the puzzle.

Analytics that cannot slow the game down

Events go through a buffered sink that drops rather than blocks. The game loop never waits on the database, and the pipeline reads the event table long after the fact.

The dashboard is code, and it is careful

The public Superset dashboard is defined in Python and reapplied on every deploy. It reads the warehouse through a read-only Postgres role that cannot see player names or the pipeline's run history — only aggregates are public. Bots and synthetic history are always labeled and filterable.

It shares the VPS on purpose

This portfolio is served from the root of the same machine, the game from a subpath. Traefik strips the prefix before the request reaches the container, so every server route stays where it was and the health checks and internal clients never learned about the move.

The repository documents every one of these as a decision record, alongside a build journey that walks through the incidents found along the way with their root causes and fixes. Read the journey.

SEE IT

The global room: a shared board with pieces, other players' cursors and the reference silhouette
The global room — shared board, live cursors, reference silhouette.
The public Superset dashboard over the warehouse, with a traffic filter separating humans, bots and synthetic history
Superset — every number comes from the nightly pipeline, not the game database.