AI SaaS

AI Invoicing SaaS — Production Hardening

Context

An AI invoicing SaaS originally built on Replit needed to move from working prototype to software that could be trusted in production. The engagement was a hardening pass over the existing codebase rather than new feature work: security, structure, validation, and tests.

The Hard Part

AI-generated apps ship working features fast but arrive with no authorization boundaries, no input validation, and no tests — the rebuild was not about features, it was about everything underneath them.

What I Built

  • Security audit of the Replit-built codebase
  • Modularization of source files that had grown past 1,000 lines
  • Strict TypeScript throughout, with Zod schemas validating input
  • Rate limiting and CSRF protection
  • Jest and Playwright test suite over the hardened codebase

The Build

Why a working prototype was not shippable

The application worked. An invoice could be created, sent, and tracked — which is what makes this category of project deceptive, because the demo is not merely convincing, it is genuine. What was missing was everything a working demo never exercises: whether the person requesting an invoice is entitled to see it, what happens when a field arrives as something other than the code assumed, and whether any of it still behaves after the next change.

Invoicing raises the stakes on all three. An invoice is a financial record naming a customer, an amount, and often an address, so an authorization gap is not a broken page — it is one company reading another's billing. And because invoices are generated once and referenced long afterwards, a value written incorrectly today surfaces weeks later inside a document somebody has already acted on.

What hardening had to work around

Three properties of the existing codebase shaped the approach.

The first is that AI-assisted builds optimise for the feature being requested. A prompt asking for invoice creation produces invoice creation; nothing in that request implies an authorization check, a rate limit, or a validation schema, so none appear. The absence is systematic rather than random, which is genuinely useful — it means the gaps are predictable and can be worked through as categories instead of discovered one at a time.

The second is file size. Source files had grown past a thousand lines, and that matters more during a security pass than during ordinary maintenance. In a file that long the same responsibility tends to appear in several places, so a check added to one of them looks complete while leaving the others open. You cannot reliably audit what you cannot hold in your head.

The third is that this was a hardening engagement rather than a rewrite. The application's existing behaviour had to survive intact, which rules out the tempting approach of rebuilding cleanly and rules in a slower sequence of narrow, verifiable changes — each one altering how the software works without altering what it does.

The order the work happened in

Sequence mattered more than any individual fix, because each step made the next one possible.

The security audit came first, since it determines what everything after it is for. Auditing before restructuring also means the findings describe the code the client actually has, rather than code that was quietly reorganised on the way to being reviewed.

Modularization followed, breaking the thousand-line files into units small enough to reason about. This is the step that looks like housekeeping and is really a prerequisite. An authorization rule applied to a module is applied once; the same rule scattered through a long file has to be repeated correctly at every call site, and the one that gets missed is the vulnerability.

Strict TypeScript with Zod then closed the input problem from both ends. TypeScript enforces shape at compile time, Zod enforces it at runtime on the boundary where data actually arrives. The distinction is the whole point: a type annotation does not survive an HTTP request, because it describes what the code expects rather than what the network delivers. A Zod schema is where a request stops being a claim and becomes a validated object, which is why the two are used together rather than treated as alternatives.

Rate limiting and CSRF protection address abuse rather than correctness. Neither is required for any feature to function, which is exactly why generated code omits them, and both become cheap to apply consistently once the routes are modular.

The Jest and Playwright suite came last, and it is what makes the rest durable. Jest covers the units now that units exist; Playwright covers the flows a user actually performs. Without them the hardening would have been a snapshot — correct on the day it was delivered and eroding from the next commit onward.

Why this case study carries no numbers

Deliberately. The outcome of hardening is the absence of events: the authorization bug that never exposed a customer's invoice, the malformed request that never reached the database, the regression a test caught before anyone saw it. None of that produces a figure, and inventing one would misrepresent the work.

What can be stated is the shape of the change. The application entered the engagement as software that worked and left it as software whose behaviour is enforced rather than assumed — typed at compile time, validated at runtime, rate limited, protected against cross-site request forgery, and covered by tests that fail the moment any of that stops being true. For an application handling other companies' financial records, that difference is the entire distance between a prototype and something you can put in front of customers.

Tech Stack

TypeScriptZodJestPlaywright
AI Invoicing SaaS — Production Hardening — interface screenshot

More Case Studies

Have a similar project?

I would love to help you build something great. Let's discuss your requirements.