← All work
01 · Full-stack AI SaaS

SupplyScout

A full-stack AI SaaS I took from an empty repo to a production-ready v2.0. It reads a restaurant's supplier invoices, pulls the line items, and shows owners where their food cost is leaking money. Built and market-researched; not yet live with paying customers.

Next.js 14TypeScriptPostgreSQLSupabaseStripeAWS TextractClaude APIPlaywright

The problem

Independent restaurants run on margins thin enough to disappear in a bad month, and most of them track food cost by hand or not at all. The data they need is sitting in a stack of supplier invoices nobody has time to type up.

I wanted to see whether an AI pipeline could do that typing for them, accurately enough that an owner would trust the number it spat out.

What I built

  • An invoice-reading pipeline on AWS Textract with a confidence-scoring and human-review step, hitting 95%+ field-extraction accuracy.
  • A 35-table PostgreSQL schema with row-level security, migrated off an early MongoDB version once the relationships got real.
  • Stripe three-tier billing ($79 / $199 / $399 a month) and authentication, end-to-end tested with Playwright.
  • A dashboard I tuned from a 3–5 second load down to under 1.5 seconds, mostly by adding 25 targeted database indexes.

Why it maps to forward-deployed work

This is the first responsibility on the FDE list: build a production application with Claude models, inside a real domain, for real users. I didn't build a demo. I built the thing an owner would actually open on a Tuesday.

The honest status: it's built, tested, and market-researched. It is not yet live with paying customers. The payback math is modeled off the market. I don't have a paying customer base yet to measure it against.

Want the deeper walkthrough? Get in touch →

How it was built

The decisions, not the demo.

Anyone can say they built a SaaS. The interesting part is the calls you make when the easy version stops working. The ones that decide whether an owner trusts the number on the screen. Here are four of them, in plain language.

Why an AI can't just read the invoice and be done

OCR will read a number wrong sometimes. On a money document that's the whole game, because an owner who catches one bad number stops trusting all of them. So every field the pipeline pulls carries a confidence score, and anything below the line gets routed to a person to confirm before it lands in the books. The machine does the typing. A person still signs off on the number. That's what separates a demo that reads invoices from a tool an owner will run their actual costs on.

Why it started on MongoDB and didn't stay there

The first version stored invoices as documents, which is the fast way to start when you don't yet know the shape of the data. Then the relationships got real. An invoice belongs to a supplier, a supplier to a restaurant, line items to invoices, prices to those items over time. Once you're asking questions across all of that, a relational database is doing the work it was built for and a document store is fighting you. Migrating later cost more than starting there would have. The signal to switch wasn't the data getting bigger. It was the questions getting harder to answer.

How a 3-to-5-second dashboard got under a second and a half

A slow page is usually not slow everywhere. It's a few queries reading more than they need to. I measured first, found the read paths the dashboard actually hit, and added 25 indexes aimed at exactly those. An index isn't a free win: it speeds reads and costs a little on every write, plus some storage. Worth it here, because owners read their dashboard constantly and write to it rarely. Benchmark, then fix what's actually slow. Guessing is how you optimize the wrong thing.

Why one restaurant can never see another's numbers

Multi-tenant means everyone's data shares one database. The app code is supposed to only ever show you your own rows, but app code has bugs, and the day it has the wrong one, a restaurant sees a competitor's costs. So the isolation lives a layer deeper, in the database itself, with row-level security. The rules about who can see what are enforced where the data actually sits, underneath the application. If the app asks for rows it shouldn't, the database still says no. You don't guard something that sensitive in only one place.