Naijapolis

Engineering blog · Part 6 of 9 · Data

Sharing without trust

Saves, city codes and the scoreboard: how one set of rules runs on both sides and nothing from outside is believed.

5 min read · 21 October 2026

All nine parts
  1. One file, no framework
  2. A kernel you can test
  3. The city’s heartbeat
  4. Ground, water and light
  5. Highlife from maths
  6. Sharing without trust
  7. Shipping and staying up
  8. Growing trees, flowers and water
  9. Waste and the politics of rubbish

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:

Open the gameAsk the networkwait a few seconds at mostanswersslow, down, or an errorShow the fresh pageand save a copyShow the saved copythe town is on the devicea late answer still refreshes the copy for next time
A small background worker keeps one copy of the game page. It prefers the network so that new versions arrive at once, but it falls back to the copy whenever the network is slow or down.

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".

Your city, in the gamebuildings, roads, land, roomsPack into a city codecompact text you can shareServer: clean it againtreat it as written by a strangerScore from layout onlysame rules as the gameStore it, label itand let people report itA visitor’s game cleans it on loadthird time, same code
The yellow boxes are one function, run in different places. Writing it once and importing it everywhere is cheaper than writing three that must agree, and far safer.

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

  1. Validate by rebuilding, not by checking and passing through.
  2. One cleaner, imported everywhere. Duplicated validation drifts.
  3. Do not accept a claim you can compute. The server derives the score from the layout instead of asking for it.
  4. 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.

Now go and build something.

It takes about a minute to lay your first road.

Play Game