Episode 1/4 of the mini-series The week Supabase lied to me 4 times. The three following episodes cover a mutation silently swallowed by the SDK [CANONICAL URL EPISODE 2: to fill in after push], an RLS recursion resolved by a JWT hook [CANONICAL URL EPISODE 3: to fill in after push], and a query that stops at exactly 1000 rows without saying so [CANONICAL URL EPISODE 4: to fill in after push].

The Tuesday the security probe spoke

It's 9:12am on a Tuesday in May. The daily drift probe has been running automatically for three weeks — an aclexplode query across all public objects, filtered on anon. I don't open it every morning. That morning, it's waiting for me with a row that has no business being there.

Niran sets a coffee on the corner of my desk without a word. He reads the output over my shoulder. A PII backup table — personal data in plaintext, created two days earlier for a bulk reclassification — shows up in the list with SELECT, INSERT, UPDATE, DELETE granted to anon. Accessible to any unauthenticated curl request. He lets three seconds pass and says: "It's not RLS." Then he goes back to his hoodie.

He's right. It's not an RLS bug. The table itself is open, at the GRANT layer, before RLS even applies.