In a serious wargame, the most important property is also the least glamorous: each cell must see only its own intelligence. The NATO/EU cell sees theirs; the Hybrid Threat cell sees theirs. If that boundary leaks, the exercise is worthless. We decided early that the boundary would be enforced by the database, not by the application — and definitely not by trust.

Why not the application layer

Application-layer authorization is where visibility bugs go to hide. A forgotten filter on one query, one endpoint that returns slightly too much, and a clever participant sees the other cell's hand. We didn't want the integrity of the exercise to depend on every developer remembering every filter. So we pushed the boundary down to row-level security in Postgres, where it is enforced once and applies to every query — cold load, realtime re-query, export, all of it.

CREATE POLICY cell_visibility ON observations
  USING (
    classification = 'OPEN'
    OR cell_id = current_setting('app.cell_id')::uuid
  );

Every connection sets its cell context, and the policy does the rest. A query for "all observations" returns only the rows the cell is entitled to, because the database refuses to hand over the others. There is no filter to forget.

Trust without trust: the boundary holds because the database refuses, not because the code remembers.

The bug we shipped on day one

We got it wrong the first time in an instructive way. Faculty need a bypass — the umpire must see every cell's world. We gave faculty a role that bypassed the visibility policy. We also, by accident, let that same bypass cover the approval requirement, so for one build a faculty session could commit consequences without the normal review gate.

The fix was to separate the two policies entirely. Visibility and approval are independent concerns, and conflating them is exactly the kind of mistake that erodes trust in the system. Faculty now have a database-level bypass on visibility views but not on approval requirements. Both are enforced in Postgres, not in application code, and neither inherits from the other.

What we learned

Put the property that must not break in the place that is hardest to break it. Row-level security is unglamorous and a little awkward to work with, and it is the reason we can promise information asymmetry and mean it. The awkwardness is the feature.

— END —