Regenerating TypeScript types straight from a live Supabase schema — instead of relying on hand-maintained interfaces — turned invisible schema drift into 234 compiler errors, including a cross-tenant security hole in a multi-tenant Next.js app that had been silently exploitable for months. Hand-written types don't fail loud. Generated types don't let you look away.
Hostwise (the AI hotel concierge agent I built) has been running two live 24-room properties for months now — bookings, guest memory, nightly reconciliation, dynamic pricing, alerting, all of it wired into a Postgres schema on Supabase that's grown to 105 tables. Like most systems built by one person shipping fast, the TypeScript types describing that schema weren't generated. They were hand-written once, then patched from memory every time a column got added, renamed, or dropped.
That's fine for a while. Then it isn't, and you don't find out until you go looking.
Regenerating the types
I ran Supabase's TypeScript type generator against the actual live schema and swapped the hand-maintained interfaces for the generated ones — a routine cleanup task, or so I thought. I rebuilt the project expecting a handful of red squiggles.
Hand-maintained types are a guess frozen in time. Generated types are read straight from what's actually deployed.
The compiler didn't complain politely. It lit up with 234 type errors across the codebase — every single one a place where the code's assumptions about the database no longer matched what was actually running in production.
What was actually broken
Type errors sound abstract until you see what they were actually pointing at. Three findings mattered:
- 01 The night audit was silently wrong. The nightly reconciliation job that closes out each day's bookings and revenue was reading a column that had been renamed months earlier. It never crashed — it just quietly computed against a stale field, so the audit numbers were off and nothing ever threw an error to reveal it.
- 02 Revenue autopilot had the same disease. The dynamic pricing engine was making pricing decisions off a field that no longer meant what the code assumed it meant. Same root cause, different feature, same silence.
-
03
A cross-tenant data leak in
hotels/route.ts. This is the one that mattered most. Once generated types forced every query result into a strict shape, fixing a type error in the shared hotels API route surfaced that it wasn't scoping its query to the authenticated user's own hotel ID at all. In plain terms: one property's login could, in principle, query another property's data. In a multi-tenant system, that's not a bug report — that's the kind of finding that ends a client relationship if someone else finds it first.
Hand-maintained types don't fail loud. They fail quiet — a wrong number in a report, a pricing decision that's off, a tenant boundary nobody's actually enforcing.
The actual lesson
Generated types don't lie, because they're not written from memory — they're read straight from what's actually deployed. Hand-written types are a snapshot of what you believed the schema looked like on the day you wrote them. Every migration after that is a chance for reality and code to quietly disagree, and TypeScript can only catch what it's actually been told is true.
The fix wasn't just regenerating types once. It's treating "regenerate types from the live schema" as a recurring check — ideally a CI step — not a one-time cleanup you do when something feels off.
This is the kind of audit I run before calling a system "done"
Not just "does it work" — does it fail loud when reality drifts from what the code assumes. If your stack has outgrown the point where one person holds the whole schema in their head, that gap is worth closing before a client finds it for you.
Talk to me about a system audit arrow_forward