Most invoice tools want you to create an account, they upload your client data to their servers, and they cost real money to run. I wanted the opposite: a tool that runs entirely in the browser, keeps invoice data on the user's device, and costs me close to nothing to operate.
It turned into Crossbill — a free VAT/GST invoice generator. Here's the architecture and the genuinely interesting bits, including the one that surprised me: even the paid tier needs no backend.
The constraint: no server, at all
The whole thing is a static site — Next.js with output: 'export' — deployed on Cloudflare Pages. There's no server, no database, and no auth. Editing, tax logic, and PDF generation all happen client-side, and the invoice data (clients, rates, amounts) lives in localStorage. It never leaves the device.
The nice side effect: marginal cost is basically zero, so I can leave the core free forever without it costing me per-user.
Money math without floating point
The first thing that bites you when you sum line items is floating point:
0.1 + 0.2 === 0.3 // false
Do that across a dozen invoice lines with tax and discounts and you'll be a cent off in ways that look unprofessional. So all the math is done in integer cents and only converted back at the edge:
export const toCents = (n) => Math.round((Number(n) || 0) * 100);
export const fromCents = (c) => c / 100;
// quantity can be fractional (e.g. 1.5 hours); round the product to whole cents
export const lineTotalCents = (qty, rate) =>
Math.round((Number(qty) || 0) * (Number(rate) || 0) * 100);
Formatting is left to Intl.NumberFormat so 25+ currencies get correct symbols, grouping and decimal digits (JPY has 0, most have 2) for free.
Generating the PDF in the browser
No headless Chrome, no server render — jsPDF plus jspdf-autotable draw the invoice directly. One gotcha: some currency symbols render as tofu boxes in the default PDF fonts, so inside the PDF I use the unambiguous ISO-code form (EUR 1,200.00) instead of the glyph — which is arguably clearer for cross-border invoices anyway.
The actual hard part: tax compliance, as data
Here's the thing — the arithmetic isn't the hard part. Any tool can multiply a subtotal by a rate. The part cross-border freelancers actually fear getting wrong is the required fields and wording per jurisdiction.
So each jurisdiction is just a plain config object, and switching it rewrites both the form and the document:
{
id: "eu-rc",
docTitle: "Invoice (Reverse Charge)",
taxLabel: "VAT",
defaultRate: 0,
sellerTaxIdLabel: "Your VAT ID",
buyerTaxIdLabel: "Customer VAT ID (VIES)",
buyerTaxIdRequired: true,
complianceNote:
"Reverse charge: VAT to be accounted for by the recipient " +
"(Article 196, Council Directive 2006/112/EC).",
}
Add AU GST (10%, "Tax invoice", ABN, "Total price includes GST"), UK VAT (20%, sequential numbering, VAT reg number), Canada GST/HST, US 1099, and the German Kleinunternehmer §19 UStG statement, and the "differentiator" is really just well-researched static data. Which is a nice reminder that a moat can be content, not code.
Monetization with no backend either
This is the part I didn't expect to work. I assumed the paid tier would force me to stand up a server. It didn't.
The Pro unlock is a one-time Lemon Squeezy purchase that issues a license key. You validate that key client-side against Lemon Squeezy's public endpoint — no secret key, and it's CORS-friendly, so the browser can call it directly:
const res = await fetch("https://api.lemonsqueezy.com/v1/licenses/validate", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ license_key: key }),
});
const data = await res.json();
if (data.valid) unlockPro();
On success the license is stored in localStorage and the Pro features light up. No backend of mine anywhere in the flow.
Tradeoffs I accepted
I'd rather be upfront about these than pretend they don't exist:
- The license check is client-side, so it's bypassable. For a $29 impulse tool, a server to stop piracy would cost more (in money and in the zero-backend promise) than the piracy it prevents. Accepted.
-
localStoragemeans switching devices or clearing your browser loses your data. A JSON export/import gives you a copy you own. - The tax presets are a convenience sourced from official guidance, not tax advice. There's a disclaimer in the UI and the PDF.
Result
It's live at getcrossbill.com — free, no signup, runs in your browser. If you invoice under a jurisdiction I've modelled wrong, or one I should add, I'd genuinely like to know.
Happy to answer questions about the client-side/no-backend approach in the comments.
Top comments (0)