---
title: "The complete ecosystem: from a live game to a build-optimization web app"
description: "Helping a player compare two equipment builds assumes a prerequisite that's easy to forget once you're looking at the finished interface: you need clean, complete game data that's…"
date: "2026-06-29T09:00:00.000Z"
updated: "2026-08-24T15:05:22.401Z"
locale: "en"
canonical: "https://ninhache.fr/en/blog/ecosysteme-complet"
author: "Néo Almeida"
tags: "architecture, wakfuli, overview"
categories: "wakfuli"
---

# The complete ecosystem: from a live game to a build-optimization web app

Helping a player compare two equipment builds assumes a prerequisite that's easy to forget once you're looking at the finished interface: you need clean, complete, up-to-date game data. Not just item stats, but their translations, their sprites, their relationships to one another, resynced with every patch the game ships.

That prerequisite alone ended up shaping five distinct systems before a single line of the comparison interface was written. This article is the grand tour: how these five building blocks fit together, and why they stayed separate instead of being merged into one project.

Before diving into the details of each block, a simplified diagram beats a long paragraph. Here's the pipeline at a glance, from the game client to the browser:

_A version number flows in a single direction, from the game to the browser._

## Block 1: decoding what the game doesn't document

It all starts with an acquisition problem. A live MMO's client stores its data in positional binary files, with no tags or field names, and this format drifts slightly with every patch. Before anything can be shown on the product side, you first need a tool able to track that drift from version to version, without rewriting the decoder by hand on every update. The same problem shows up for the game's tooltips, which aren't plain text but a small DSL with its own references and its own formatting logic. This block stays deliberately the most closed off of the five: it's the one that touches the game client directly.

[Tracking a proprietary MMO's binary format across its patches](/en/blog/suivre-format-binaire-mmo)

[Reverse-engineering a tooltip templating language with no documentation, one spell at a time](/en/blog/reverse-dsl-tooltips)

## Block 2: aggregating, loading, and separating what's public

Once the data is extracted, it still needs to be made usable. The ETL hub takes the raw output from the previous block, cross-references it, enriches it, and publishes it in two forms: versioned JSON artifacts on one side, a populated Postgres database on the other. This is also the layer where a distinction plays out that runs through the rest of the pipeline: some data is public (the data that ends up in the product), some stays internal (the data that only serves the pipeline itself). This hub's code is organized around a fairly strict hexagonal architecture, so that the input adapters, the raw formats that change with every patch, stay decoupled from the core business logic.

[A public/private data tier from a single pipeline](/en/blog/tiers-donnees-public-prive)

[Hexagonal architecture for real, not just in theory](/en/blog/hexagonal-en-pratique)

## Block 3: rendering a sprite with no graphics card

One of the artifacts produced by the ETL hub is the characters' animation files and texture atlases. The sprite rendering service, written in Rust, takes these artifacts and composes a PNG image on demand, server-side, with no GPU available. The constraint isn't producing a nice-looking image but producing the exact same image the game client would display, pixel for pixel. This service runs apart from the others because its workload, CPU-intensive rasterization, and its lifecycle have nothing in common with those of a typical web server.

[A CPU software rasterizer that has to match a GPU pixel for pixel](/en/blog/rasterizer-cpu-vs-gpu)

## Block 4: the product the player sees

This is the visible block: a web app with Next.js on the front end and AdonisJS for the API, letting a player build a piece of equipment, compare it to others, and share it. It consumes the Postgres database populated by the ETL hub, queries the rendering service to display sprites, and relies on Redis to cache the most frequent reads. Two pieces deserve their own dedicated article: how the database resyncs with a new export without ever taking the service down, and searching for items by visual hue rather than by name, built on a color distance in Lab space.

[Zero-downtime data reseeding with a homemade command](/en/blog/reseed-sans-downtime)

[Color-similarity search: cube, GiST, and a handmade Lab converter](/en/blog/recherche-couleur-lab-gist)

## Block 5: serving images without reinventing a CDN

Last block, the most modest in code size but not in usefulness: a small service that serves images (item icons, rendered sprites, illustrations) with resizing and caching, deliberately stopping short before adding features a real CDN already handles better. That choice of scope is what keeps this service tenable at ninety lines rather than a few thousand.

[Why I wrote a 90-line image CDN instead of paying for a service](/en/blog/cdn-images-90-lignes)

## The thread that ties the five together

What makes these five repositories cohere isn't a shared deployment or a shared language (there's decompiled Java, Rust, TypeScript, SQL), it's a version number that propagates in a single direction.

**A patch ships**

The game releases a patch, which triggers block 1.

**Versioned decoding**

Block 1 produces new versioned artifacts.

**Aggregation**

The ETL hub consumes them and publishes its own aggregate version.

**Consumption**

Rendering and the web app each pull the artifact version that concerns them.

**Reseed**

Down to reseeding the product database.

At no point does a downstream block guess at or recompute a piece of data

 that an upstream block should have already handed it fully resolved.

## What these five blocks taught me

The temptation, early on, would have been to put everything into a single monolithic repository: after all, it's the same project. But each block has a completely different rate of change, language, and load profile. The binary decoder only runs when a patch ships. The rendering service runs continuously but needs very little persistent memory. The web app, meanwhile, moves at the pace of a product with live users. Forcing them into the same deployment would have coupled their lifecycles for no good reason.

**Lesson**

Separating these blocks forced a useful discipline: every boundary between two repositories is an explicit interface, with a versioned data format, that can't be worked around by quietly importing an internal function from another module.

This discipline, applied to a context where you control neither the source format nor its rate of change, is the thread running through everything else.

[Designing around data you don't own](/en/blog/concevoir-autour-donnees-quon-ne-possede-pas)
