All nine parts
A city is only fun if you can show it to someone. But the moment a game lets one player’s data reach another player’s screen, it has a problem: anything that arrives from outside might have been written by someone who wants to cause trouble. This part is about how Naijapolis lets cities travel without ever trusting them.
We will stay with principles and leave out the details of our limits and defences. A good security design should still hold when everything about the approach is public; the specifics of a particular deployment are another matter.
Local first
A player’s city lives on their own device. There is no account needed to play, and no server needed to build. The save is a compact structure the game writes to the browser’s storage and reads back on the next visit. This choice has a nice side effect on engineering: the server is not on the critical path of playing, so a server problem can never stop someone building their town.
Because there is no server round trip for most actions, the game also survives a flaky connection. The same idea extends to loading the game itself:
Two rules keep it safe. The worker touches only the game page: calls to the server go straight to the network and fail the way they always would, and the game copes. And every release gets a new copy of the worker with a new version number, so old copies are cleaned out rather than piling up.
A city is a program’s worth of data
A saved city is a list of buildings (each with a position, shape, colours and optional furnished rooms), roads and fences (lists of points), and optionally the land. It can be turned into a short text called a city code that players can copy, post or link. Anyone can paste a code, which means a code can say anything, including nonsense:
- a road with a million points,
- a house at a position outside the map,
- a colour that is not a colour,
- a price that is negative,
- a field with the wrong type altogether.
Clean on the way in. Every time.
The defence is not a list of known-bad inputs. It is a cleaner: a function that takes anything and returns something guaranteed to be well-formed, by rebuilding the value from only the parts it understands.
// the shape of the idea (simplified)
const num = v => typeof v === 'number' && isFinite(v);
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
function cleanHouse(raw) {
if (!raw || typeof raw !== 'object') return null; // not even an object
const pts = Array.isArray(raw.pts) ? raw.pts.slice(0, MAX_POINTS) : [];
const clean = pts
.filter(p => Array.isArray(p) && num(p[0]) && num(p[1]))
.map(p => [clamp(p[0], -MAP, MAP), clamp(p[1], -MAP, MAP)]);
if (clean.length < 3) return null; // cannot be a house
return { t: 'house', pts: clean, color: cleanColor(raw.color) }; // build new, never reuse raw
}
Notice the last line: the result is a new object built from checked parts. Copying the raw input and "fixing" it would let an unexpected field slip through. Building fresh makes that impossible.
The second rule is that this cleaner runs everywhere a city enters a system: when the game loads a save, when it opens a visited city, and when the server receives one. It is the same function, in one file, imported by all three. There is no "server version" that could drift away from the "game version".
A scoreboard nobody can fake
Cities can be ranked. The obvious way, trusting a number the game sends, would let anyone claim any score. So the game never sends a score. The server works the score out itself, from the one thing it can inspect: what is built, and where.
That needs a scoring function that depends only on the layout. No money, no mood, no population, nothing that lives in a player’s running game. The function holds everything else at a neutral setting (a normal tax rate, no arrears, no floods) and uses the very same service, power and mood rules as the live game. The numbers differ slightly from the in-game ones, so the scoreboard uses its own version everywhere, in the game and on the server, and a score can always be recomputed by anyone who has the layout.
A second check asks whether a layout could plausibly have been paid for in the time a player has been known. A city that costs a hundred years of income to build, submitted on day one, is not ranked.
Making abuse boring
A few design habits make life harder for troublemakers without adding a lot of code:
- Everything shared can be reported, and enough reports can hide it until a person looks.
- Everything shared can be deleted by its owner, and deleting a player removes their shared things.
- The public pages, including this one, run under a strict content policy: no inline scripts and no inline styles at all, so even a mistake in the page cannot run injected code.
- Analytics are opt-in and anonymous, and the landing page that you are reading this on carries none of the game’s code.
What we learned
- Validate by rebuilding, not by checking and passing through.
- One cleaner, imported everywhere. Duplicated validation drifts.
- Do not accept a claim you can compute. The server derives the score from the layout instead of asking for it.
- Keep the server off the critical path of play. It makes failures small.
The last part follows the code out of the editor: the tests that guard it, and the machines that serve it.