actuallyfrank blog
writing in publicno analytics on this pagerss still works
All posts
changelog

Nobody can read your sessions

nobody-can-read-your-sessions

The last change gave Gamemood a front door and admitted that nothing behind it knew what a game was. This one is the part that makes the app mean anything: a catalog, sessions, and the rule about who may read them.

Gamemood's whole claim is that it measures something. Every screen in the design set carries a percentage and the n behind it, and the check-out screen promises you that "nothing you pick here is public — sessions only ever show up as aggregates on a game's page". Both of those were, until now, sentences in an HTML file.

The hard part is not the table

A session is easy: a game, how you felt before, how you felt after, whether it was worth it. The hard part is that the app publishes numbers computed from rows nobody is allowed to read.

There is no API service in this repo — the server code queries Postgres directly — so Row Level Security is the whole boundary, not one layer of several. Your session rows are readable by you and by nobody else. Not by another signed-in person, not by an anonymous visitor. The anonymous role does not merely fail the policy check; it has no privilege on the table at all, so Postgres refuses one step earlier.

The public numbers come from a small set of functions that run as their owner and return only aggregate columns. There is no row id in their output, no owner, no note, no timestamp — not by convention, but because the shape has no column to put one in. A future caller cannot select something that leaks a row, because there is nothing there to select.

Two consequences fall out of that for free. A rate and its sample size come back from the same call, so "every percentage carries its n" is structural rather than a thing the UI remembers to do. And the curated list — the hand-picked "here, try this" — lives in a table with no rate column at all, so a curated pick can never be rendered with a percentage attached. A bug cannot violate a rule the schema will not express.

The result card promises a session stays editable for 24 hours. That is a database predicate now, plus a trigger that refuses to move the completion timestamp. A promise the client enforces is a promise the client can withdraw.

The catalog had other plans

The design said: import Steam's whole app list from a keyless endpoint, roughly 250,000 entries in one response, and never call Steam on the request path.

That endpoint no longer exists. It answers 404 Method 'GetAppList' not found in interface 'ISteamApps' and has been removed from Steam's own list of supported methods. The only bulk source left needs a Web API key.

So the import got a key-based path and a keyless fallback that searches by term and builds a deliberately partial catalog, which is enough to make a fresh checkout usable. And a free Steam key moved from "would be nice eventually" to a prerequisite for launch. Plans about other people's APIs have a shelf life.

Fixtures lied

Everything above passed its tests against a handful of fixture rows. Then I imported all 239,485 Steam apps and ran the search that the check-in flow depends on.

Typing "hades" one key at a time cost 1.67 seconds of database time. A single character took over a second on its own. The index was not the problem and neither was the planner: a trigram is three characters wide, so a shorter query gives the index nothing to look up and Postgres has no choice but to read every row. Sixty-seven times faster once a three-character floor went into the function — not the search box, so a future screen cannot forget it.

Then the ranking. Sorting every game above every add-on works beautifully on five fixture rows and fails completely on a real catalog: with results capped at twenty, a game's own soundtrack sat below a few hundred loosely-matching titles and never appeared at all. The rule that add-ons are shown and labelled rather than hidden was, in practice, unsatisfiable.

And the heuristic for spotting an add-on had been written from the design set's examples — soundtrack, OST, DLC, demo. Searching "powerwash" returned twenty rows of which eighteen were DLC packs labelled as games. Steam's most common DLC naming is a "Special Pack" or a seasonal drop, and none of it was covered.

Three real defects, none of which any amount of local testing would have found, because the fixtures were too kind. The catalog is not a detail of this feature. It is the feature's actual operating conditions.

What is still not true

The database is right and the numbers are honest, and there is still no screen that writes a single row of it. Check-in and check-out come next.

One thing survives all of this unresolved, and deliberately: a withheld session still exists in a table an operator could read. The promise on the check-out screen is about the community numbers. It should not be allowed to quietly grow into a promise about me.