Magic links and the weight of a live database
Day three of the build. Auth was wired, Postgres was up, seventeen surfaces were talking to a real database. Before that day, every bug was a learning moment. After, every bug was a potential customer-data problem.
Day three of the build. Supabase auth was wired. Postgres was up. Seventeen surfaces of the app were talking to a real database with real row-level security policies. Eighteen migrations had landed. The thing booted, the thing logged in, the thing wrote actual rows.
Before that day, every bug was a learning moment. After that day, every bug was a potential customer-data problem.
A prototype is a sandbox. A product is responsible.
The first-solo moment
There is a pilot equivalent. A student flying solo for the first time is technically in the same airplane as the one they flew with an instructor. But the airplane suddenly weighs more. Every decision is theirs. Every consequence lands on them.
That's what wiring real auth and a real database does to a side project. The airplane gets heavier.
The implication that hit me at 11 PM that night: I now had to start thinking about row-level security as a security perimeter, not a convenience. If I wrote a SELECT policy wrong, one CFI could potentially see another CFI's students. That is not a "bad UX" problem. That is a "lose all my customers and possibly my reputation" problem.
The discipline shifted immediately. Every migration since has a WITH CHECK clause on every INSERT and UPDATE. Every RPC that returns pre-auth data is SECURITY DEFINER and explicit. The default became "deny," not "allow."
Why Supabase, briefly
I chose Supabase for one reason: Postgres plus row-level security in one box, with no vendor lock-in. If the platform disappears tomorrow, I can lift the database and run it elsewhere. The product survives the platform. That was non-negotiable for a one-person company.
You don't build a flight school on a runway you don't own.