Selected Work - Engineering

Streetvers - a roguelike & card platform

A single-player street-culture roguelike for mobile and desktop, paired with a collectible character-card surface that drives the game's social and influencer reach - every card is both promo content seeded to influencers and the player's unlock for the character it depicts. A deployed FastAPI backend, a deployed TanStack Start admin panel, and a Unity 6 game client, built and operated solo on a hardened self-hosted stack - in collaboration with Reality Metaverse, the Web3 studio behind Landlord GO.

Role

Solo Developer

Stack

Unity 6 · C# · FastAPI · SQLAlchemy 2.0 · PostgreSQL · TanStack Start · React 19 · Ubuntu VPS

01Overview

Streetvers is a single-player roguelike on mobile and desktop, wrapped in a card-driven growth engine where every character is also a collectible card. I own the FastAPI backend, the TanStack Start admin panel, and the Unity 6 client across four platforms, built in collaboration with Reality Metaverse, whose $RMV economy my backend reads over its API rather than re-implementing. The backend and admin are live in production, and the Unity prototype is at version 0.4 and not yet wired to the API.

02What I built

  • The game (Unity, mobile + desktop) - a single-player roguelike on the MoreMountains TopDownEngine: a wave / combat loop, a buff-card economy with 33 buff types drafted between waves, a territory base layer of 10+ capturable zones with hired guards/dealers and passive income, a raid simulator, a sigil meta-progression skill tree, and a monetisation-store UI. One Unity 6 project builds to iOS, Android, Windows and Mac.

  • Card-driven growth surface - the same character data that drives the roguelike's playable roster doubles as collectible promo cards - authored in the admin panel (portrait, stats, rarity, lore tags), then seeded as social drops aimed at influencers and surfaced as stake-gated drops in Reality Metaverse's $RMV staking program (backers who stake receive pre-launch cards, whitelist spots and VIP access). The marketing asset and the in-game unlock are the same object - one render path, several audiences.

  • Backend (FastAPI) - the source of truth for cards, characters and rosters - 29 endpoints across auth, characters, abilities, teams, players, admin users and health. Polymorphic id-or-slug lookups, 8 stat fields with a server-canonical FIFA-style overall-rating formula, soft deletes, pagination/filtering, and a player ↔ card collection (PlayerCard M2M) that is the system of record for which characters a player can play.

  • Admin panel (TanStack Start) - 16 routes; full CRUD for characters (with a live StreetCard preview and an in-form OVR readout that mirrors the server formula), abilities, teams, players (with their collection), and admin users. Role-aware UI with super_admin / editor / viewer gating.

  • Auth & permissions - JWT with role + admin_user_id / player_id claims and a token-version revocation counter that invalidates outstanding tokens the moment an admin is deactivated. bcrypt hashing with a timing-attack-equalising dummy hash on failed login. Per-IP rate limits on /auth/token and /auth/register.

  • Image pipeline - dual-mode storage in the API: Cloudinary in production, local filesystem in dev, behind a single service interface. Portrait uploads land on the correct backend without the form ever knowing the difference.

  • Dual-mode admin - the admin panel's service layer swaps the real API for an in-memory mock at build time via VITE_USE_MOCK, so the panel runs and demos end-to-end with no backend.

  • Hardened self-hosted infrastructure - per-app system users, default-deny outbound firewall, AIDE filesystem-integrity + auditd syscall trails, an rrsync-restricted deploy pipeline that cannot run arbitrary commands, and PM2 file-watch in place of an SSH-and-restart step in CI.

03Architecture

Unity game

Unity 6 · offline · planned wiring

FastAPI

29 endpoints · JWT · 5 models

Admin panel

React 19 · TanStack Start · Vite

PostgreSQL 14

5 tables · 3 migrations

The backend is a self-hosted FastAPI service in front of PostgreSQL 14 on a small Ubuntu VPS, managed by PM2 behind nginx with Let's Encrypt TLS. The admin panel is a TanStack Start app under its own user and nginx vhost. The Unity client ships to four platforms from one project and runs offline today, with Cloudinary serving portrait images in production.

Streetvers x Reality Metaverse - official trailer

04Technical challenges

API designed before its caller

Problem
The Unity client is offline today, but its first call will read the player's roster and gate character selection, so the contract had to be right now.
Approach
I shaped the API around that future caller. GET /characters/{id} accepts an int id or a slug, player auth issues a JWT with a player role, and I recorded each shape decision as a numbered ADR. The admin panel hits the same endpoints every day.
Result
When the game connects, the API won't need to change, because the admin workflow has been testing that same contract all along.

One rating formula, two runtimes

Problem
The same character appears in the admin form, on social, and in the game, and its rating cannot differ between where staff author it and where players see it.
Approach
I kept the weighted overall-rating formula canonical in the Python API and mirrored it exactly in the admin panel's computeOverallRating, and the form renders the same StreetCard component the game reuses.
Result
Staff preview the exact card a follower and a player will see, and the rating never disagrees across the three surfaces.

Least-privilege deploy pipeline

Problem
A normal deploy step hands the CI key a shell on production over SSH, so a leaked GitHub Actions secret would mean a full compromise.
Approach
I pinned the deploy key with rrsync so it can only rsync files to one path and run nothing else, dropped the restart step by letting PM2 watch the build directory, and ran each app under its own user behind a default-deny firewall.
Result
The worst case from a leaked CI secret is stale files in a folder, not a shell on the box.

Token-version JWT revocation

Problem
Staff and players share one auth system, and when an admin is deactivated their existing tokens have to stop working immediately, not at the next expiry.
Approach
Each JWT carries the user's token_version, and the decode path re-checks it on every request and rejects a mismatch, so deactivating an admin just increments a counter. Failed logins run a dummy bcrypt check so timing never reveals whether a username exists.
Result
Revoking access is a single UPDATE, and login attempts fail in constant time whether or not the account exists.

05Stack & why

FastAPI and PostgreSQL behind the data, Unity 6 for one game across four platforms.

Unity 6C#MoreMountains TopDownEngineFastAPISQLAlchemy 2.0PostgreSQLalembicPydanticJWTbcryptCloudinaryReact 19TanStack StartTanStack QueryViteTypeScriptTailwind 4shadcn/uiPM2nginxUbuntu 24.04GitHub ActionsAIDEauditd

06Outcomes

StatusBackend + admin in production · Unity client in pre-release 0.4
Productionadmin.streetvers.com · api.streetvers.com (Let's Encrypt TLS)
29HTTP endpoints (FastAPI, across 6 routers)
5 / 3SQLAlchemy models / alembic migrations
8pytest files covering auth, token revocation, RBAC, character smoke
15architectural decision records (adr/)
16admin-panel route files (TanStack file-based router)
190 / 9Unity C# scripts / scenes
33buff card types in the roguelike loop
10+capturable territory zones in the base layer
Token-version JWTone-row revocation on admin deactivation
Hardened VPSper-app users · default-deny egress · AIDE + auditd · rrsync deploy gate
Card surfaceOne render path · several audiences (admin form · social drop · stake-gated drop · in-game unlock)
PartnerReality Metaverse - $RMV staking + NFT economy integrated over API