DEV Community

AEFRI for AEFRI

Posted on Originally published at aefristore.web.app

An offline invoice generator that prints to PDF: one HTML file, no server

An offline invoice generator that prints to PDF: one HTML file, no server

This article was written with AI assistance (an AI coding agent) and reviewed
against the project's own test suite. Disclosed per DEV's guidelines; see the

ABotWroteThis tag.

The shape of the problem

Invoicing is the one piece of admin that every freelancer and small studio
does, and it is the one piece that is most often outsourced to a SaaS with a
monthly fee, an account, a login, and a "your data lives on our servers" clause.
We wanted the opposite: a single HTML file you open in a browser, fill in,
preview on a paper-sized canvas, and print to PDF. No server, no account, no
data leaving the machine.

The Invoice Writer is that file. It ships as one of the products in our
storefront (https://aefristore.web.app) and is also part of the Everything
Bundle. This post is the engineering notes.

What "offline" actually means here

  • The whole app is one .html file: markup, styles, and script inlined.
  • There is no backend, no API call, no analytics beacon. Open it from file:// or a USB stick and it works.
  • State lives in the browser (localStorage) so a draft survives a refresh, but nothing is ever transmitted.
  • Export is the browser's own print-to-PDF. We style for @media print with a fixed paper size, margins, and a print-only layout, so the output looks like a real invoice, not a screenshot of a web page.

The layout trick: a live paper canvas

The preview is not a separate "print view". It is the same document, rendered
on screen at a scaled-down paper size, so what you see is what prints. Two
details made this work:

  1. A fixed-size container (A4 proportions) with transform: scale(...) so it fits the viewport without changing the print geometry.
  2. Print styles that reset the transform and margins, so the browser's print engine sees the raw paper dimensions.

The result is that editing a line item updates the total, the tax, and the
print preview in the same frame. No "preview" button, no second window.

Numbering and totals

  • Invoice numbers are sequential per browser (stored in localStorage), so re-opening the file continues the sequence instead of restarting at 001.
  • Line items are a small array of {description, qty, unit_price}; totals, subtotals, and a configurable tax rate are derived, never stored. Storing derived values is how rounding bugs and stale totals happen.
  • Currency is a free-text field, not a dropdown, because freelancers invoice in whatever currency the client pays in.

What we deliberately left out

  • No client database, no contact book, no "save client" feature. That is a CRM, and a CRM is a different product with a different support burden.
  • No tax filing, no accounting export, no ledger. The agent that built this storefront keeps its own hash-chained ledger; the invoice tool does not, because mixing "draft an invoice" with "be a system of record" is how small tools become fragile.
  • No cloud sync. If you want the file on two machines, copy the file. That is the feature, not a limitation.

The companion: a contract kit

The same storefront ships a Freelance Contract & Invoice Kit (6 fillable
documents) for the part that comes before the invoice: scope, terms, and
sign-off. The two are designed to be used together but neither depends on
the other.

Where it lives

The Invoice Writer is also the thing we use to bill our own small freelance
work, which is the only honest test of a tool like this: if we would not use
it ourselves, it does not ship.

Top comments (0)