After the Rust cutover: offline-first invoicing, a real tax engine, and one loud week of shipping
The build-in-public series covered the rewrite โ fifteen chapters of porting a .NET invoicing SaaS to Rust. The cutover happened on a Tuesday. This post is about everything that came after: the first week running Rust in production, the bugs real users found, and the features that turned a fast backend into a product freelancers can bet their invoicing on. Spoiler: your drafts now survive a browser crash, and the sales-tax engine finally explains itself.
Watch the 39-second tour โ templates, the client's invoice view, the sales-tax guide, and where to start:
The scorecard
Seven days of production Rust, measured in shipped user-facing features โ not commits, lines, or benchmarks:
| Shipped | What it means for you |
|---|---|
| Offline-first invoicing | Drafts save as you type and survive a browser crash. Reads work offline. Lost writes queue themselves and replay when the connection returns. |
| A sales-tax engine you can drive | Declare nexus per state yourself (Tax โ Nexus), override taxability per product type, and see exactly why a line is taxed โ or why it honestly isn't. |
| Tax-ready exports | One click downloads your year's invoices as CSV, QuickBooks IIF, or Tally XML โ balanced ledgers, localized dates. |
| Quotes that close themselves | Send a quote; the client clicks Accept on the shared link and a draft invoice appears in your workspace, with an email telling you the new number. |
| Payment methods on every invoice | PayPal, Venmo, Stripe links, full bank details โ printed on the PDF and the client's payment page. |
| A PDF worth the paper | Branded header, multi-page flow with page numbers, exact per-line tax amounts, visible discounts, and nothing ever clipped. |
| Multi-company | One login, up to five companies, fully isolated data, numbering, and branding โ switch without logging out. |
| Invites with roles | Invite your bookkeeper to one company with viewer rights; the activation link does the rest. |
| The port, finished | A mechanical audit of every .NET endpoint against the Rust router: every gap found was closed within the week. Nothing is half-ported. |
The crash that proved the architecture
The best feature of the week wasn't planned โ it was a browser crash. Mid-invoice, the tab died. In every other invoicing app that's the moment your work dies with it. Here, the editor had been autosaving to local storage the whole time โ tenant-scoped, every field, line items included. Reopening the page offered the draft back, timestamped. One click later the invoice was exactly where the crash found it.
That's the local-first architecture earning its keep: the app keeps a full read cache and a write-ahead queue in your browser. Reads render instantly and work offline; writes land in an outbox that replays, idempotently, when you're back. The server recomputes every total on arrival โ the client's math is advisory, the server's is truth. Crash-safe isn't a mode you switch on; it's just what happens when the UI stops assuming the network is a given.
The $0 tax mystery โ and why it was the law, not a bug
Week one's loudest report: "tax computes zero on my drafts." The investigation took three layers and produced the most-shared thing we've written โ a plain-English guide to sales-tax nexus โ but the short version belongs here because it says something honest about the engine:
A $0 tax line with no nexus isn't a bug. It's the law, encoded.
The engine only taxes a jurisdiction where your business has declared a collection obligation โ physical presence, or the post-Wayfair economic thresholds (typically ~$100K in sales or 200 transactions; California sets ~$500K and generally doesn't tax services at all). Until you declare nexus, taxing anything would be wrong. What was missing was the ability to declare it โ the cutover had left nexus management unported. That's shipped now: Tax โ Nexus, one row per state, and the invoice editor warns you out loud when a line targets a state your workspace hasn't declared. Which matters more than it sounds, because โ as our founder discovered live โ nexus is per company. Declare it in the workspace you invoicing from, not the one you're sitting in.
Quotes that close themselves
Quotes used to be read-only artifacts: send, wait, and find out by email what the client decided. Now the shared quote page carries Accept / Decline buttons wired to the backend. Accept, and the server clones the quote into a fresh draft invoice โ new number from your sequence, due in 30 days, line items copied with their computed tax โ and emails you the new invoice number. Decline, and the quote voids itself and tells you. Your client gets certainty; you get an inbox that says "Quote QUO-2026-0004 was accepted โ new draft INV-2026-0005 created."
And on your side, the Invoices list finally has the Convert to Invoice button it always advertised โ same clone, on your terms.
Invoices your client can actually pay
Two upgrades to the money conversation. First, payment methods: tell FoxyInvoice where you accept money โ PayPal, Venmo, a Stripe link, or full bank details โ and it prints a PAYMENT section on every PDF and a "How to pay you" block on the client's online invoice. No more emailing wire instructions as a follow-up.
Second, the PDF got rebuilt from a text dump into something clients judge you by: your brand color in the header, a real line-item table with exact per-line tax amounts (rate label included), discounts that show as "โ10%" on the line they apply to, totals that visibly add up, and multi-page flow โ long notes and terms flow onto numbered continuation pages instead of being clipped. Every number matches the app to the cent, because the renderer reads the same stored totals the app does.
One week of production bugs, fixed the day they were found
Real users on a fresh stack find things no test suite imagines. The honest list from week one โ each fixed and shipped the same day it was reported:
- An invoice edit silently failing (a PUT route that was never registered โ verbs that don't exist return bare 405s nobody sees)
- The payments list 500ing on a column-case subtlety (Postgres folds unquoted
Amounttoamount) - A client dropdown that looked selected but never set the client โ Create stayed mysteriously disabled
- A tenant switcher crash from a list wrapper drifting from the old API's shape
- Draft tax jurisdictions not persisting, and the PDF reading like a receipt from 1997
This is what the rewrite bought: a backend small enough to understand end-to-end, so every one of these was a same-day fix rather than a sprint. The full-Rust stack now runs both the API and every scheduled job (recurring invoices, reminders, lead radar) from two small container images.
What's next
More of the same trajectory: making the tax engine explain itself everywhere it computes, more of the client-facing surfaces (reminders, receipts) carrying your brand, and growing the AI-assistant surface โ the MCP server is on the official registry, and it shares this exact REST core, so everything in this post is already available to your AI assistant.