Case study
PLANIT: keeping each family's data separate
A family planner is the kind of app where a data leak is personal. This is how I made separation the database's job instead of the app's, how I tried to break it, and the mistakes I made while doing so.
The decision
PLANIT is a phone-first planner with a calendar, chores, meals and groceries, a scanning document vault, notes and shared money. It has no application server. The browser talks to Supabase (Postgres, auth, private storage and realtime), and the site itself is static.
That meant every authorization rule had to live somewhere, and I put all of them in Postgres row-level security. The data is small and relational, and the rules fit in policies. The reason that matters: application code can forget to check, and a policy cannot be skipped by a new screen. One family can never read another family's rows because the database refuses, not because the app remembers to ask.
Trying to break it
I treated my own database as the thing to attack. The security tests sign in as different people against a real database and check what each can and cannot do: an anonymous visitor, a member of another family, a child, a parent. At the September 2026 audit that suite held 110 database checks and passed. The audit also found two high-severity problems and several smaller ones, all fixed:
High
Joining with the family code and typing the name of a parent who had already been added granted the Parent role.
Fix: Joining now always creates a Kid. Only an existing parent can promote someone.
High
Realtime updates were subscribed to every family's changes, so a deletion anywhere made every client refetch.
Fix: Database triggers now broadcast to a private channel for each family, with row-level security on the channel itself.
Medium
A child could rewrite who created a parent's grocery item, then delete it.
Fix: An ownership trigger on every content table stops creators being rewritten.
Medium
Invite codes never expired and could be guessed without limit.
Fix: Codes expire after 7 days, regenerating resets them, and wrong guesses are rate-limited per user.
What a child could see
Later I signed in as a child against a real database and found that they could read the whole money ledger and every calendar event, including ones meant only for the parents. The fix was a parents-only ledger, a lock on individual events, and per-child switches for what each child may see and change.
Before shipping those switches I attacked them from a child's account and found three routes around my own work. A child could join the chore history to the family list and read a sibling's points by name. A storage-usage function reported file counts that included parents-only files. Switching off event creation stopped a child making events but not editing ones they already had, including moving one onto a sibling. None of the three was in the interface. Each was a place where I tightened one rule and left its neighbour alone, which is exactly what tests that only follow the happy path never find.
Mistakes worth recording
- A test reached production. My browser walk-through imported the real database client instead of the stand-in, so it sent requests to the live project. The database refused them, because anonymous reads return nothing and anonymous writes have no grant, and I verified the family's data was untouched. Tests now receive the stand-in explicitly.
- A query would have shipped blind. One query named its columns, and an update added seven the app reads, so every permission switch would have shown as off. Neither suite caught it, because one only checks that named columns exist and the stand-in ignores column lists. Four tables now load all columns, and a test records why.
Where it stands
PLANIT is live with 243 passing tests and a Help guide written for a family rather than a developer. It works offline for what has already loaded. The privacy policy and terms are drafts: I have not had them reviewed by a lawyer, and the pages say so. The repository is private, so the live app and this write-up are the evidence.