What Changed After I Shipped Margus
Margus shipped in April with six Supabase Edge Functions and permissions that were basically an honor system. By September it had four times the backend surface area, real access control based on role, and a Playwright suite that caught a feature the redesign had quietly broken before it ever reached the client. None of that happened during the original build. All of it happened after, once I'd circled back to figure out what the app actually still needed to be.
Features nobody asked for at launch
The biggest of those was a proposal workflow that didn't exist when the app first shipped. At launch, a quote was a row in a table. Now a quote can be turned into a generated PDF proposal, sent out to the client, marked accepted or declined, and once a signed copy comes back in, the system reads that upload and creates a real project from it automatically. None of that pipeline existed in April. It meant adding a PDF generation library and writing several new backend functions just to move a quote through that whole lifecycle without someone retyping the same information three times.
The other big addition was proper access control based on role, with row level security in the database tied to a user's role and which projects they were actually assigned to. Early on, permissions were closer to an honor system than a security model, because the only user who'd mattered up to that point was the office manager running the whole show from one screen. Once more than one person was going to need access at a time, some in the office and some potentially out on job sites, that honor system stopped being good enough. A site supervisor didn't need to see every contractor's payment history, and a subcontractor certainly didn't need to see another subcontractor's rates. That kind of boundary can't live in a UI condition that just hides a button. It needed to be enforced at the database, so that even an honest mistake in the frontend couldn't leak data it shouldn't.
The backend that quietly grew four times over
The original build shipped with around 6 Supabase Edge Functions. By September that number was closer to 24. Some of that growth was the proposal workflow. A good chunk of it was things I hadn't anticipated needing at all.
Webhooks from Xero, it turned out, weren't reliable enough to trust on their own for payment reconciliation. Events would fire late, or not at all, or twice. So alongside the webhook handler I added a scheduled reconciliation job that periodically checks Margus's payment records against Xero directly, plus a webhook health check that flags when deliveries go quiet. Neither of those functions is glamorous. Both of them exist because trusting a single delivery mechanism for financial data turned out to be naive.
The rest of the growth was a notifications system and a set of audit logging triggers on the database, the kind of infrastructure that doesn't show up in a demo but matters the first time someone asks "who changed this invoice and when."
A redesign, and a bug that only existed because of it
Somewhere in that same stretch, the whole frontend moved from its original UI approach to Tailwind CSS v4 and shadcn/ui. Along the way I folded Sheets into Dialogs, since they'd been doing overlapping jobs, and flattened a navigation structure that had gotten deeper than it needed to be.
That redesign is also where I found one of the more interesting bugs I've hit on this project. An AG Grid table had a row click handler, onRowClicked, wired to the native DOM click event rather than React's own synthetic event system. On the screen for invoices awaiting confirmation, clicking "Review" was supposed to open a payment confirmation modal. Instead, because the grid's click handling ran independently of React's event tree, a read only invoice detail drawer also opened underneath the modal, every time, for every single person who clicked that button. It wasn't rare or intermittent. It happened for everyone, and it had been happening for a while before anyone flagged it as strange rather than just a visual glitch.
Building the net after the acrobatics
Here's the part of this story I've thought about the most. The original build didn't include a Playwright suite. Neither did the redesign. It wasn't until after the Tailwind and shadcn migration was already merged and shipped that I finally sat down and built a proper end to end suite for Margus.
The first full run of that suite was on August 15th. It came back with 42 failing tests spread across roughly ten spec files. Almost all of them were stale selectors, tests written against class names and structures that the redesign had already replaced weeks earlier. That part was tedious but expected; selectors rot, and fixing them is routine maintenance.
What wasn't routine was what showed up buried in that same batch of failures: an entire feature, editing an existing project, had been silently broken by the redesign, and I hadn't caught it either. Nothing crashed, nothing threw a visible error, the page just quietly stopped doing the one thing it was supposed to do, which is exactly the kind of gap that's easy to miss if you're clicking through a redesign checking that things look right rather than deliberately trying to break each feature. It had been sitting broken since the redesign shipped, and a test suite that was only a few hours old caught it on its very first run, before the client ever got a build with that gap still in it.
Sit with the order of events there for a second. Access control based on role was built before the safety net existed. The proposal workflow was built before the safety net existed. The redesign that broke project editing happened before the safety net existed. The very first time that safety net actually ran, it immediately proved its value by catching a real regression that had been sitting there, silently, since the redesign shipped. I didn't build the tests first and then feel reassured by them. I built the features, then the redesign, then finally the tests, and the tests spent their first few minutes of existence proving that I should have built them sooner.
By the time I'm writing this, Margus has 1,322 unit tests and a Playwright suite of 27 specs that runs against a real Supabase backend and a real Xero demo sandbox tenant rather than mocks standing in for either one. That's a lot of coverage for a project that shipped with none of it in April. I'm glad it exists. I just wish it had existed before the things it was built to catch.
Small fixes that mattered anyway
Not every fix that stretch was dramatic. A shared drawer component used across the app was rendering form labels without an htmlFor attribute connecting them to their inputs. Visually nothing looked wrong. For anyone using a screen reader, though, every label in every drawer in the app was disconnected from the field it described, and that's exactly the kind of gap ordinary clicking around never surfaces, since it only shows up if someone deliberately tests with a screen reader instead of just looking at the screen. It sat there for a while before anyone thought to check.
Turning a ratchet, not a switch
Near the end of this stretch I added eslint-plugin-sonarjs to the project. By September, Margus was a substantially bigger codebase than the one I'd shipped in April, and I already knew from other projects that flipping a strict lint rule on all at once against a codebase that size produces a wall of violations nobody has the appetite to fix. So instead of enabling it everywhere at once, I set it up as a ratchet: enforced on files as they're touched or explicitly cleared, rather than as a blanket rule across everything that already existed. New code meets the bar immediately. Old code meets it gradually, as it comes up for other reasons. It's a smaller decision than everything else in this post, but it came directly out of watching the codebase grow past the size where rules applied all at once make sense.