Stu Mason
Stu Mason

How I Rescued a 22-Table Mess and Rebuilt It in 10 Clean Tables

Stuart Mason8 min read

Jamie came to me with a problem I've seen too many times. He had a vision — a platform for the underground music scene that would connect DJs, promoters, and venues. He'd had a prototype built. It loo

How I Rescued a 22-Table Mess and Rebuilt It in 10 Clean Tables

Jamie came to me with a problem I've seen too many times. He had a vision — a platform for the underground music scene that would connect DJs, promoters, and venues. He'd had a prototype built. It looked decent in demos. And underneath, it was an absolute state.

This is the full story of how I rescued it.

What I Found

Before writing a single line of code, I spent two days just reading. Reading the schema, reading the components, reading the API routes, mapping what connected to what. This is the most important phase of any rescue project and the one most developers skip because it's not billable in the traditional sense. Bollocks to that. You can't fix what you don't understand.

Here's what the audit revealed:

22 database tables. More than half were either completely unused or duplicating data that belonged in another table. There were venue-related tables for a feature that was never built. There was an event_performers table that was trying to be three different things at once — a lineup tracker, a booking system, and an invitation workflow. There were columns that referenced tables that didn't exist anymore.

1,000+ line components. React components that handled data fetching, state management, business logic, form validation, and rendering all in one file. You couldn't change one thing without understanding everything. And since there were no tests, you couldn't verify that your change didn't break something else.

30+ dead code files. Files that were generated or partially built and then abandoned. Imports that pointed to modules that no longer existed. Functions that were defined but never called. The kind of thing that happens when you're prompting an AI and iterating fast without cleaning up.

Broken email queues. The notification system was configured — there were actually quite well-structured notification classes. But the queue processing was broken, so nothing actually sent. Users were signing up and getting no confirmation emails. That's not a minor issue.

Zero tests. Not a single test. No unit tests, no feature tests, no integration tests. Nothing. This meant there was literally no specification for what the code was supposed to do. Every fix was a guess.

Scattered authentication logic. Auth checks in some routes, missing from others. No consistent authorization policy. Security holes that would let any authenticated user access any other user's data.

The agencies Jamie had spoken to quoted six figures. I can see why — if you approach this as "fix everything that's wrong," the scope is enormous. But that's the wrong approach.

The Approach: Surgical, Not Scorched Earth

The temptation with a messy codebase is to throw it all away and start fresh. Sometimes that's the right call. But not here. The domain knowledge was embedded in the code. The data model — buried under the mess — reflected real business concepts that Jamie had spent months thinking through. The UI, while poorly implemented, showed the actual user journeys that mattered.

So instead of a rewrite, I did a systematic rescue. Ten phases, each focused on a specific concern, each buildable on the last.

Phase 1: Domain Mapping

Before touching any code, I mapped the actual domain. What does Rezzy really need? Strip away the dead features, the half-built ideas, the "maybe we'll need this" tables. What are the core entities?

  • Users (DJs, promoters, and fans)
  • Events (the thing being created and shared)
  • Event Invites (how DJs get invited to play events)
  • Messages (communication between users)
  • Follows (social connections)
  • Media (profile images, event artwork)

That's it. Ten tables to model this properly, not twenty-two.

Phase 2: Schema Redesign

This was the scariest commit of the whole project. One atomic migration that:

  • Dropped 12 unused tables
  • Restructured the remaining tables with proper foreign keys
  • Replaced the chaotic event_performers table with a clean event_invites table modelling a proper workflow: pending, accepted, declined
  • Added proper indexes
  • Enforced data integrity with database-level constraints

One migration. Atomic. If anything failed, it rolled back cleanly. This is why you don't "gradually refactor" a broken schema — you design the right schema and migrate to it in one go.

Phase 3: Dead Code Deletion

This was therapeutic. 59,000 lines deleted. Dead components, unused utilities, orphaned API routes, half-built features that were never finished. If it wasn't referenced by anything that was actually running, it went.

Some developers are nervous about deleting code. "What if we need it later?" That's what git history is for. Dead code isn't harmless — it confuses developers, shows up in search results, and makes the codebase feel bigger and scarier than it is.

Phase 4: Type Migration

The original code was a mix of TypeScript and JavaScript with types applied inconsistently. I standardised everything to TypeScript with strict mode. This immediately surfaced dozens of bugs — places where the code was passing the wrong data shape, accessing properties that didn't exist, or handling null values incorrectly.

Types aren't bureaucracy. They're documentation that the compiler enforces. On a rescue project, strict typing is the fastest way to find bugs without running every possible user flow manually.

Phase 5: Component Rewrites

Those 1,000-line components got broken apart. The event creation flow went from one massive file to a set of focused components — each handling one concern, each testable in isolation.

The key principle: each component should be describable in one sentence. "This component renders the event details form." "This component displays a list of invited DJs." If you can't describe it in one sentence, it's doing too much.

Phase 6: API Restructuring

The Laravel API was rebuilt with proper Actions, DTOs, Form Requests, and Policies. Each endpoint got:

  • A Form Request that validates input
  • An Action that contains the business logic
  • A Policy that checks authorization
  • A DTO that shapes the response

This is the Laravel Actions pattern I use on every project. It separates concerns cleanly and makes everything independently testable.

Phase 7: Security Hardening

Proper authorization on every endpoint. Policies that check whether the authenticated user actually has permission to access the requested resource. Rate limiting on authentication endpoints. CSRF protection on all forms. Input sanitisation.

The original had routes where any authenticated user could access any event's private data just by changing the ID in the URL. That's not an edge case — it's a security hole.

Phase 8: Real-Time Messaging

The messaging system was rebuilt using Laravel Reverb for WebSocket connections. Clean channels, proper event broadcasting, message persistence, and read receipts. The original had a polling-based approach that hammered the server every 3 seconds.

Phase 9: Mobile Foundations

React Native app via Expo, sharing types and API contracts with the web app. The benefit of the clean API from Phase 6 is that the mobile app just consumes the same endpoints — no separate backend needed.

Phase 10: Test Coverage

59 tests. Covering the critical paths: user registration, event creation, invite workflows, messaging, authorization policies. Not 100% coverage — that's a vanity metric. But enough to deploy with confidence and refactor without fear.

The Numbers

Let me give you the raw numbers because they tell the story better than prose:

  • Tables: 22 down to 10
  • Lines deleted: 59,000
  • Tests added: 59 (from zero)
  • Commits: 82
  • Duration: 3 months
  • Cost: A fraction of the six-figure agency quotes

What I Learned (Again)

Every rescue project reinforces the same lessons:

Understand before you act. The two days I spent reading code before changing anything was the most valuable time in the project. Without that understanding, I would have rebuilt things that didn't need rebuilding and missed problems that weren't obvious from the surface.

Delete aggressively. Dead code is not harmless. Kill it. If you need it back, it's in git. You won't need it back.

Schema first. The database schema is the foundation. If that's wrong, everything built on top of it will be wrong. Fix the schema first, then fix the code.

Tests are the exit condition. How do you know the rescue is done? When you have enough test coverage to deploy with confidence. Not when the code looks pretty — when you can prove it works.

Rescue is a different skill. Building greenfield and rescuing existing projects require different mindsets. Greenfield is creative — you're designing solutions. Rescue is archaeological — you're uncovering intent buried under bad implementation. Both are valuable. They're not the same thing.

If you're sitting on a codebase that sounds like what I've described — bloated schema, no tests, dead code, giant components — it's probably not as bad as you think. Most of the code doesn't need to be there. The actual product underneath is usually smaller and simpler than the mess suggests.

You just need someone willing to dig it out.


I write about Laravel, AI tooling, and the realities of building software products. If you found this useful, there's more on stuartmason.co.uk.

Get the Friday email

What I shipped this week, what I learned, one useful thing.

No spam. Unsubscribe anytime. Privacy policy.