← All writing

The Invoice Is an App

An invoice is three computations: Σ(price × quantity), a tax rate applied to the subtotal, and a sum. Mere builds one in under 90 lines of declarative HTML — reactive computed chain, two screens, no JavaScript. The document and the app were always the same thing.

Michael Shatny··7 min read

The Invoice Problem

An invoice seems like a document. It has a sender, a recipient, a date, a list of line items, and a total. You print it. You email it as a PDF. It is a record, not a program.

But every invoice you have ever seen was actually computed. Someone opened a spreadsheet, typed quantities and prices, and the subtotal calculated itself. The tax was a formula. The balance due was a sum. The “document” was quietly a running program the whole time — one that required Excel or Google Sheets to host it, and surrendered the user's data to a cloud provider the moment they clicked “save to Drive.”

The invoice was always an app. Mere just makes that explicit.

What an Invoice Actually Is

Strip it to primitives. An invoice is:

1

State

Named values — sender, recipient, line items, tax rate, dates. Editable. Persistent across sessions.

2

Computed state

Values derived from state — subtotal (sum of price × qty), tax amount (subtotal × rate), balance (subtotal + tax). Reactive: change a quantity, the balance updates instantly.

3

Two views

A presentation screen (the invoice as it would appear to the client) and an edit screen (fields and a spreadsheet for line items). Same state, two renderings.

That is the complete structure. Mere encodes it in markup — no JavaScript, no framework, no build step.

Three Lines of Computed State

The financial logic of any invoice is three dependent computations. In Mere, that is three lines in a <computed> block:

computed chain
<computed>
  <value name="subtotal"   from="items"              op="sum-product" field="price" by="qty" />
  <value name="tax-amount" from="subtotal,tax-rate"   op="percent-of" />
  <value name="balance"    from="subtotal,tax-amount" op="add" />
</computed>

Three operations: sum-product, percent-of, add.

sum-productΣ(price × qty)

Iterates the items list and sums the product of two fields per row. No stored per-row amounts — the total derives from the raw data.

percent-ofa × (b / 100)

Applied to subtotal and tax-rate. Change the tax rate field, tax-amount recomputes immediately.

adda + b

Subtotal plus tax-amount. The cascade is automatic — editing a line item quantity propagates through all three values.

The cascade is deterministic. Edit quantity in row 2 → subtotal updates → tax-amount updates → balance updates. All three in a single reactive pass. No event handlers. No useEffect chains.

The Line Items Table

The invoice screen shows a read-only data table. Each row computes its own amount column — price × quantity — without storing that value in state:

invoice screen — line items
<data-table @items>
  <column field="description" label="Description" />
  <column field="price"       label="Price"  as="currency" />
  <column field="qty"         label="Qty" />
  <column field="price"       label="Amount" as="product" by="qty" />
</data-table>

The as="product" column type is a render-time computation: it takes the field and multiplies it by by per row, formats as currency, and displays the result. Nothing is stored. The source of truth for every amount is always the raw price and quantity in state.

On the edit screen, the same list renders as a <spreadsheet> — an inline editable grid with the same columns. Click a cell, type a new value, hit enter. The computed chain responds immediately in both directions: edit any quantity in the spreadsheet, and the totals on the invoice screen are already updated.

The Totals Block

Totals render as <kv> rows — a label on the left, a formatted value on the right. Each one reads a computed value and formats it:

invoice screen — totals
<heading>Total</heading>

<kv @subtotal   format="currency" label="Subtotal"    />
<kv @tax-rate   format="percent"  label="Tax rate"    />
<kv @tax-amount format="currency" label="Tax"         />
<kv @balance    format="currency" label="Balance due" />

The @ sigil binds each <kv> to a state or computed value. Change the tax rate in the edit screen: the “Tax rate” row updates, the “Tax” row updates, the “Balance due” row updates — all three in the same reactive pass, with no JavaScript written to wire them together.

Two Screens, One File

The invoice has two screens: one for viewing, one for editing. They share all the same state — there is no synchronization, no copy, no save button. The screens are just two different windows into the same declared values.

navigation between screens
<navigation-bar "bottom">
  <nav-item "invoice">Invoice</nav-item>
  <nav-item "edit">Edit</nav-item>
</navigation-bar>

The <nav-item> takes the target screen name as its content argument. No action, no handler, no routing configuration. Switch screens, and all the @-bound elements re-read from the same state they were always reading from. Everything is consistent by default.

Persistence is one attribute. Add persist to any state value and it survives browser closes — written to OPFS with a localStorage fallback. Every field in the invoice persists: the sender details, the line items, the invoice number, the tax rate, everything. Close the browser, reopen it, and the invoice is exactly where you left it. No server. No account. No sync.

The Live Workbook

Below is the invoice running in your browser right now. Switch to the Edit tab, change a quantity or add a line item. The totals on the Invoice tab update in real time. This is the complete workbook — one file, no JavaScript, no dependencies.

invoice.mp.html — classic-light theme · persistopen ↗

The Complete Workbook

The full source — 89 lines of declarative HTML. State, computed chain, two screens, navigation, persistence. Everything:

invoice.mp.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Invoice — Mere</title>
  <script src="/mere-runtime.js"></script>
</head>
<body>
<workbook theme="classic-light">

  <state>
    <value name="from-name"      type="text"   value="Semantic Intent"              persist />
    <value name="from-address"   type="text"   value="Brooklyn, New York"           persist />
    <value name="from-email"     type="text"   value="[email protected]"            persist />
    <value name="to-name"        type="text"   value="StratIQX"                     persist />
    <value name="to-address"     type="text"   value="Strategic Consulting Division" persist />
    <value name="invoice-number" type="text"   value="INV-0042"                     persist />
    <value name="invoice-date"   type="text"   value="Apr 17, 2026"                 persist />
    <value name="due-date"       type="text"   value="May 17, 2026"                 persist />
    <value name="tax-rate"       type="number" value="8"                            persist />
    <value name="notes"          type="text"
      value="Payment due within 30 days. Bank transfer or PayPal accepted." persist />
    <value name="items" type="list" value='[
      {"id":"1","description":"Strategy workshop","price":3200,"qty":1},
      {"id":"2","description":"Cascade analysis (hourly)","price":145,"qty":12},
      {"id":"3","description":"Intelligence framework","price":1800,"qty":1},
      {"id":"4","description":"Reporting & handoff","price":95,"qty":4}
    ]' persist />
  </state>

  <computed>
    <value name="subtotal"   from="items"              op="sum-product" field="price" by="qty" />
    <value name="tax-amount" from="subtotal,tax-rate"  op="percent-of" />
    <value name="balance"    from="subtotal,tax-amount" op="add" />
  </computed>

  <screen name="invoice">
    <header>
      <heading>Invoice</heading>
      <subtitle>@invoice-number</subtitle>
    </header>
    <heading>@from-name</heading>
    <paragraph>@from-address</paragraph>
    <paragraph>@from-email</paragraph>
    <heading>Bill to</heading>
    <paragraph>@to-name</paragraph>
    <paragraph>@to-address</paragraph>
    <kv @invoice-number label="Invoice #" />
    <kv @invoice-date   label="Date" />
    <kv @due-date       label="Due" />
    <heading>Line items</heading>
    <data-table @items>
      <column field="description" label="Description" />
      <column field="price"       label="Price"  as="currency" />
      <column field="qty"         label="Qty" />
      <column field="price"       label="Amount" as="product"  by="qty" />
    </data-table>
    <heading>Total</heading>
    <kv @subtotal   format="currency" label="Subtotal"    />
    <kv @tax-rate   format="percent"  label="Tax rate"    />
    <kv @tax-amount format="currency" label="Tax"         />
    <kv @balance    format="currency" label="Balance due" />
    <paragraph>@notes</paragraph>
    <navigation-bar "bottom">
      <nav-item "invoice">Invoice</nav-item>
      <nav-item "edit">Edit</nav-item>
    </navigation-bar>
  </screen>

  <screen name="edit">
    <header><heading>Edit</heading></header>
    <heading>From</heading>
    <field ~from-name    placeholder="Your name" />
    <field ~from-address placeholder="Address" />
    <field ~from-email   placeholder="Email" />
    <heading>Bill to</heading>
    <field ~to-name    placeholder="Client name" />
    <field ~to-address placeholder="Client address" />
    <heading>Invoice</heading>
    <field ~invoice-number placeholder="Invoice #" />
    <field ~invoice-date   placeholder="Invoice date" />
    <field ~due-date       placeholder="Due date" />
    <field ~tax-rate       placeholder="Tax rate %" type="number" />
    <heading>Line items</heading>
    <spreadsheet @items>
      <column field="description" label="Description" editable />
      <column field="price"       label="Price"  editable format="currency" />
      <column field="qty"         label="Qty"    editable format="number" />
    </spreadsheet>
    <heading>Notes</heading>
    <field ~notes placeholder="Payment terms, notes…" />
    <navigation-bar "bottom">
      <nav-item "invoice">Invoice</nav-item>
      <nav-item "edit">Edit</nav-item>
    </navigation-bar>
  </screen>

</workbook>
</body>
</html>

The Boundary That Was Never Real

The separation between “document” and “application” was always a boundary imposed by tooling, not by the nature of the things themselves. A spreadsheet is a document that computes. An invoice is a document that reacts. A form is a document that captures.

Every time we built these things as web apps — with servers and databases and JavaScript bundles and auth flows — we were solving a hosting problem that only existed because we chose a format that required hosting.

Mere does not solve the invoice problem by building a better invoice app. It solves it by making the file reactive. The invoice is a file. The file runs. You send it. The recipient opens it. Their changes stay on their machine. The data is theirs.

The document and the app were always the same thing. It just took a different format to make that visible.

Try it: open the workbook above and edit a line item quantity.

Watch the subtotal, tax, and balance update in real time — no JavaScript written, no event handlers, no framework. Then open the raw file and read the source. Everything you see is declared, not programmed.

Resources

Michael Shatny is a software developer and founding contributor to .netTiers (2005–2010), one of the earliest schema-driven code generation frameworks for .NET. Mere continues a 28-year pattern: structured input, generated output, sovereign artifacts. The file is always enough.

ORCID: 0009-0006-2011-3258