The game is 100% static (vanilla JS + HTML5 Canvas, no build) served by GitHub Pages.
The only thing that goes out to the network is the chat with the bums: a fetch to our
proxy. And that request crosses a whole self-hosted Kubernetes infra. This is the full end-to-end
journey of one message. Nothing is magic or someone else's cloud: it's our own hardware, and
everything is declared via API.
Two different things travel: the game's static files (HTML/JS/CSS) and the AI chat. The static files can be served from GitHub Pages (today) or from our own infra (nginx in the cluster); the chat always goes through the self-hosted infra. Same browser, two origins:
[ Browser / GitHub Pages ] the game (static)
│ HTTPS fetch(llm-tormenta-solar.cybercirujas.club)
▼
[ Public DNS + WAN IP ] A → home IP
│ :443
▼
[ HAProxy (edge, Mac mini G4 · OpenBSD) ] TCP mode · SNI passthrough
│ forwards raw TCP by hostname (doesn't terminate TLS)
▼
[ Cilium Gateway API 192.168.178.200 ] ← TLS terminates HERE (Let's Encrypt)
│ cilium-envoy + HTTPRoute (hostname → Service)
▼
[ Service → Pod: tormenta-ai-proxy ] Node · CORS · personas · guardrails
│ POST /v1/chat/completions
▼
[ LiteLLM (the central router) ] key pool · fallback · routing
├──────────────┬──────────────┐
▼ ▼ ▼
[ OpenRouter ] [ NVIDIA GPU ] [ RK1 NPUs ]
cloud · free HAMi + Ollama 4× local inference
Vanilla JS + Canvas, no framework, no build, hosted on GitHub Pages. The chat does a
fetch POST with the NPC, your message and a bit of context. Your API key is optional:
by default it hits our proxy (free); if you set an OpenRouter key, it stays only in your browser as an
override. The server's "real" key never touches the client.
The domain llm-tormenta-solar.cybercirujas.club resolves to the home public IP. The
certificate is from Let's Encrypt, issued by cert-manager using a DNS-01 challenge via
acme-dns — so there's no need to expose :80 or validate over HTTP. The cert renews
itself.
The edge is the most "junkyard-hacker" part of all: a PowerPC Mac mini G4 running
OpenBSD — yes, a ~2005 machine recycled as a TLS router. HAProxy lives there in TCP mode
with SNI passthrough: it reads the req.ssl_sni from the TLS hello and, depending on
the hostname, forwards the raw TCP to the right backend — without decrypting anything (TLS is
terminated by the gateway, further in). Several domains share the same backend pointing at the cluster
VIP. Here we tune maxconn and timeout so long LLM responses don't get cut.
Traffic enters the Kubernetes cluster through the cluster-gateway (GatewayClass
cilium), a fixed VIP served by Cilium LB-IPAM. TLS terminates here: a per-host
HTTPS listener presents the certificate (the Secret filled by cert-manager). It's Gateway
API, not Ingress: routing is a declarative, standard resource.
An HTTPRoute matches the hostname and routes to the proxy's Service. The data plane is cilium-envoy, acting as an HTTP reverse-proxy inside the cluster. Adding a new domain is, quite literally, adding an HTTPRoute and a listener — without touching HAProxy beyond the SNI rule.
tormenta-ai-proxyA tiny Node service (our own image, arm64). It does three things: sets the CORS headers so GitHub Pages can call it; keeps the personas (each bum's system-prompt) server-side; applies the guardrails (if the model is slow, it returns the "the solar storm is interfering with the model" line instead of leaving you hanging). Then it forwards the request to LiteLLM with the real key, which never leaves to the browser.
A single OpenAI-compatible endpoint (/v1/chat/completions) that's the routing
brain. It keeps an API key pool, does fallback between models if one fails or saturates,
and decides where to send each request based on the model_name. Switching from "cloud" to
"own hardware" is changing a model name — the game has no idea. The chat currently uses a
free model (Gemma family) by default.
Behind LiteLLM there are three interchangeable destinations:
The idea: start free in the cloud and, when it makes sense, move the chat to our own hardware without touching the game or the proxy.
Hubble (from Cilium) shows every L3/L4/L7 network flow in the cluster: you can watch, live, the request leaving the proxy toward LiteLLM and on to inference. Prometheus scrapes the metrics (LiteLLM exposes requests, latency, spend, fallbacks) and Grafana charts them. If something is slow or failing, you see it on a dashboard, not blind.
The proxy image is built inside the cluster with Kaniko orchestrated by Argo Workflows (no Docker daemon or external CI needed), on an arm64 node, and pushed to an internal registry. The deploy is a Helm chart that creates everything declarative: the HTTPRoute, the Certificate, and even an idempotent hook that adds the HTTPS listener to the shared gateway. Standing it up on another cluster is one command.
The deploy is itself an Argo Workflow (tormenta-deploy): it builds → rolls out →
runs a smoke test against the real URL and, if anything fails, does an automatic rollback to
the previous version and pings you on Telegram. That same alert once caught a broken signage cron.
And every night a QA autoplay plays the level by itself (headless) and reports if something broke —
all in-cluster, no humans in the loop.
None of this was done "by hand and let's see if it works": Gateway and HTTPRoute (Gateway API), Certificate and ClusterIssuer (cert-manager CRDs), the build (Argo CRD), the deploy (Helm values). Everything is a versioned object that re-applies the same way every time. That's what makes a home-grown infra serious: it's reproducible.
The game isn't hardcoded: it runs from a data model (a level JSON) that a loader turns into rooms.
Things are entities + components (interact, fight, be a billboard, have memory, have a social graph).
A new level = change data, not code. A parity test guarantees the data-driven engine produces
exactly what the old hardcoded one did. Dispatch is by registries (verb→handler), no giant
if/else; even balance is data.
You sneak into the chino's back room and a level is GENERATED that runs in the real engine (you jump, there are enemies, camera, real art). The interesting part: the AI can propose a thousand layouts, but only playable ones get through. A formal validator (the "net") checks the level is traversable (doors not blocked, exit reachable) before loading it; if it fails, it re-asks or auto-repairs. Fun fact: the net even catches a bug a human introduced (kept as a regression test).
The hobo-oracles chat with AI and have memory: they remember what you talked about. With that, the oracle can invent a level tailored to what you seem to be into (it gathers your chats and the AI builds the theme, the lines, and even picks the structure). Your memory → the generated world. NPCs also know the ecosystem (your progress, the billboards, the World Cup) and gossip among themselves via a social graph.
The cinema has a "LIVE" floor: a screen showing the living world (how many are playing now, in which area, a ticker of anonymous milestones). And above it a top-down bodegón where you actually run into other players, seated at the tables, with their emotes and preset phrases. The interesting design bit: the server is a relay with no authority (each client posts its state over SSE and receives everyone else's; nobody is "in charge"), it lives in the same proxy (dependency-free Node, in-memory), and because public chat is preset + emotes (no free text) no moderation is needed. If the network drops, the bodegón is a bar you can play solo and nobody notices the online was missing.
Two features where players actually build something shared, persisted on the server (PVC disk): The Board — you leave a short note that lives on the server until ANOTHER player reads it, and then it's gone (consume-on-read, à la Death Stranding); and the community Datacenter — a single GLOBAL state everyone contributes parts to until it's complete, with seasons and an endgame cutscene. Backend in the same proxy (on-disk bank, quotas + rate-limit + censoring) plus an AI cron that also contributes content. All additive: with no network, the game runs the same.
Truco isn't just any mini-game: it's a pure engine (no global state, 100% functions) with envido, flor, truco/retruco and the sung calls, covered by tests (hundreds of unbiased games). And it's played 1v1 and 6-player (3v3) against other humans: matchmaking runs on the server (real tables, not fragile P2P), the match runs host-authoritative over the same lounge transport, and each player's views don't reveal the opponent's hand. With the 3v3 house rule and AI bots filling any empty seats.
Quests aren't a pile of ifs: they're a graph of steps (data) with a generic runtime that
advances on its own based on which NPC you talk to (by role, not by a fixed name). So a whole quest —say,
getting out a chip a robot planted in you: you switch character, get a trojan, buy a retro console and use
it— is described as data and plugs into the engine without touching the loop's code. The hobos know
everything and drop you hints about the step you're on (they don't spell it out), and milestones split
into main (the chain) and side (the standalone ones).
Every AI feature degrades to static when the hardware (the GPU/models) isn't there. There's a circuit breaker: if a call fails or stalls, it opens for a while and everything falls back to static content instantly, without hanging on timeouts. When the GPU comes back, it closes on its own. Same for the news/ads banks and the chat: there's always a plan B.
A Telegram bot wired to Hermes (an agent already running in the same cluster) to run the game from Telegram chat: administer it, generate new content and orchestrate the world from your phone. The game gets a conversational "control panel", on the same infra.