Pulso is a watch collection tracker, and the obvious next feature is letting a friend see your collection. It is also the first feature I have built where getting it wrong means publishing somebody else’s private data rather than showing them a wrong number.
So before writing any of it, I had three different permission models designed independently, one enforcing the rules in the database, one in a single server function, one in database views that physically cannot select the forbidden columns. Then I had each of the three attacked six ways: read what someone paid, read a hidden watch, read a stranger’s collection, make a friend’s browsing cost me money, write to somebody else’s data, get connected using only a code you found.
Every model passed the attack I was worried about. Not one of them leaked a purchase price, a serial number or a private note. The column filtering worked in all three. If I had reviewed this myself, that is exactly where I would have looked, and I would have concluded it was safe.
Two forbidden facts turned out to be reconstructable from permitted ones. The wear calendar was on my list of things that are fine to share, and I had argued that case out loud, because it records only which watch on which day, with no location and no time. It also records the first day you ever wore it. On my own collection, that reproduces the purchase date to within five days on four of the nine watches where I can check. Purchase date was on the never share list. The calendar was handing it over.
The second one is worse. Market value and what I paid are separate fields, with separate switches, and I treated them as separate facts. On my own data they are the same number on two of ten watches, and within twenty per cent on nine of ten. Sharing what a watch is worth was effectively sharing what I paid for it.
Neither of those is a leaked column. Both are private facts rebuilt from public ones, which is the category of mistake that reading a schema will never show you, because every individual field is behaving.
The fix is to share shapes instead of values. Wear history now goes out as month buckets with counts rather than dates, and value goes out as a band rather than a number. You can see that someone wears a watch often, or that a watch is in the low thousands, without either fact being precise enough to reverse.
The other finding was not about data at all. An invited friend, in every version of the design, was a full account on my app rather than a viewer. Five separate attacks used that: draining a shared search quota, triggering price refreshes that multiply by the number of people looking, spending research credits on page load. I had spent all my attention on what a friend could read and none on what a friend could cost me.
What I refused to do is ship it anyway. Three of the decisions I had already made and written up turned out to be wrong, so the schema I was about to write that evening would have been the wrong schema. Building it would have meant rebuilding it. It is a strange feeling to spend an evening producing nothing but a corrected set of assumptions, and it is obviously cheaper than the alternative.
The lesson I will actually keep is that the review has to attack the thing, not read it. Reading finds the mistakes you already know how to make.