DEV Community

Lith SEO
Lith SEO

Posted on Originally published at foxyinvoice.com

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.


Try it on a real workspace: FoxyInvoice is free to start — create one at foxyinvoice.com/login and redeem founding code U8B4Z8S87X on the Upgrade page for 6 months of Pro, free, no card.

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.
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 short version 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're invoicing from, not the one you're sitting
in. (We wrote the whole mental model up in
When should you charge sales tax?.)

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.
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 Amount to amount)
  • 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 client-facing surfaces 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.


Try it on a real workspace: FoxyInvoice is free to start — create one at foxyinvoice.com/login and redeem founding code U8B4Z8S87X on the Upgrade page for 6 months of Pro, free, no card.

Top comments (0)