What it does buy you
A backup is a file copy. A migration to another host is moving one volume. An auditor asking for seven years of records gets a database, not a CSV approximation of one.
Brontiq Books is an invoicing and bookkeeping system for freelancers, contractors and small businesses. It is local-first in the literal sense: the database is a single file on a volume you own, running the same container on your laptop as it does in production. What comes out of it is a compliant Australian tax invoice, and the whole ledger is scriptable, so an invoice can be raised by a sentence rather than a form.
The phrase is worth being precise about, because it is used loosely. Brontiq Books is not an offline browser app that queues writes and merges them on reconnect. It has no service worker and no client-side replica, and claiming otherwise makes a nicer story than the true one.
What it is instead is a system where the database is not someone else's. Brontiq runs on
PocketBase, which is a single Go binary with SQLite behind it. The whole ledger, every invoice,
payment, credit and BAS period, is one file on a mounted volume. In production that volume is
attached to a machine in Sydney. On a laptop it is a directory called pb_data next
to the source, served by the same container image, with the same migrations applied in the same
order.
That is the property that matters to the person whose money is in it. There is no export step required to get your data out, because there is no format it is trapped in. You can stop paying for the hosting and keep working. For a ledger, which is a record you are legally required to retain and may need to produce years later, that is a different guarantee from a SaaS form over a database you cannot see.
A backup is a file copy. A migration to another host is moving one volume. An auditor asking for seven years of records gets a database, not a CSV approximation of one.
Close the laptop lid on a plane and the app stops working like any other web app. Offline capture is not built, and the page does not pretend it is.
Invoicing is deliberate desk work, not field capture. The valuable property is that the record cannot be held hostage, and that is the one it delivers.
An invoice is not a receipt with a logo on it. In Australia it is a document with legal requirements, and the thing Brontiq issues is built to be that document: the words Tax invoice, the supplier and their ABN, the client, an invoice number, the date and the due date, each line with the GST that applies to it, and the GST total stated separately from the amount payable.
The detail that gives the model away is where GST is computed. It is not ten per cent applied to
the subtotal at the foot of the page. Every line item carries its own gst_percent,
defaulting to 10, and the invoice total sums the per-line GST and rounds the sum. That
distinction is invisible while everything on the invoice is taxable, and it is the entire
difference the moment one line is not.
A line on a Brontiq invoice is quantity × unit_price, which covers a fixed
price as a quantity of one and an hourly line as a quantity of hours. Adding a line item
recomputes the parent invoice: it sums every line total, sums the per-line GST, rounds both, and
writes the result back, so the list view and the document can never disagree with each other.
The invoice record itself also carries is_hourly, hourly_rate and
hourly_quantity for invoices billed as a single hourly engagement. This produces the
one piece of API behaviour worth knowing before you use it: changing the rate or the hours
alone will not move the money. The recalculation is guarded on the flag arriving in the
request body, not on the flag stored against the record, so a PATCH must resend
is_hourly: true alongside the new figures to trigger it.
PATCH /api/invoice/{id}
{
"hourly_quantity": 42
}
// stored is_hourly is true, but the
// guard reads the request body, so
// amount, gst and total are untouched
PATCH /api/invoice/{id}
{
"is_hourly": true,
"hourly_quantity": 42
}
// amount = 42 x 165.00 = 6,930.00
// gst = amount x 0.1 = 693.00
// total = amount + gst = 7,623.00
That files easily under trivia. It reads better as a decision about who is allowed to move money. A PATCH that silently re-derived a total from whichever fields it happened to touch lets any partial update rewrite an issued invoice as a side effect. Requiring the caller to name the billing mode makes the recalculation an explicit instruction rather than an inference, which is the behaviour you want from the endpoint an automated agent is holding.
The same instinct shows up in the recompute helper, which refuses to act twice over. An invoice with no line items keeps its stored totals rather than being zeroed, and an invoice whose line items all sum to zero while the record carries a real amount is left alone as well, on the reasoning that those lines are notes rather than charges. Both guards exist so that a routine recalculation cannot quietly destroy a real figure.
The two paths are not identical, and the difference is instructive. The line-item recompute reads each line's own GST rate and sums them; the hourly shortcut multiplies by ten per cent directly, because a single hourly engagement is one rate by definition. The general path is the one that stays correct when an invoice is mixed, which is why it is the one the document is built from.
Small-business invoicing tools are usually a closed form. Brontiq exposes the ledger over an HTTP API behind a bearer token, compared in constant time, which is what allows an invoice to be raised from a sentence typed at an assistant rather than from a screen. Clients are matched by name rather than by identifier, so the caller does not have to hold a database key to bill someone. The match runs in tiers, exact first, then containment either way, then word overlap with a floor of half the words agreeing, and a name that matches nothing is refused rather than quietly creating a second client.
Two of those are worth pulling out. Marking an invoice paid sets the payment date automatically when one is not supplied, and moving the status back off paid clears it again, so the pair cannot drift out of agreement. And deletion is a soft delete written through a log rather than a row disappearing, which is the correct behaviour for a financial record and the reason the interface carries a deletion log of its own.
A statement answers one question: what does this client owe today. Brontiq builds it per client, listing every invoice with its status and payment date, and reconciling to a balance. Status is the interesting part, because overdue is never stored. It is derived at read time by comparing the due date to now, and paid invoices are excluded from the test. An invoice becomes overdue because the calendar moved, not because a nightly job remembered to run.
Worth being straight about the rest of it. The other statuses are set by a person, not inferred from the payments table, so recording a part payment does not move an invoice to a partial state on its own. And the statement is delivered as a print view rather than a generated file: the client opens it and prints to PDF. The invoice is the document that is properly generated; the statement is a screen that knows how to print.
The schema history reads as the honest account of the product. It starts with clients, invoices and payments, which is the whole of an invoicing tool. Then line items arrive, because a single amount on an invoice stops being enough. Then a deletion log, because financial records should not vanish. Then credits, expenses, tax settings, GST compliance fields on expenses, and BAS periods, at which point the thing has stopped being an invoicer and become a bookkeeping system that knows what an Australian quarter is.
Alongside that a client portal appears, with its own logins, so a client can see their own statement without an account being created for them. Those logins are PIN-based, and the PIN is stored as a salted PBKDF2-SHA-256 hash at 200,000 iterations rather than a bare digest, with older hashes upgraded on the next successful login. A four to six digit PIN has a small enough keyspace to be exhausted trivially from a leaked bare digest, so the salt and the iteration count are doing real work there. Portal views and downloads are logged with their time and origin, so the operator can see what was accessed.
The most recent migration in the repository is the one that closed the door properly: read access on every financial collection now requires an authenticated request. The commit that introduced it says plainly what it was fixing, which is the correct way to record that kind of change rather than quietly amending it.
A supplier PDF can be uploaded and read rather than retyped. Extraction is a model call, so that step is the one place the local-first claim genuinely does not hold.
Payments are their own records against an invoice, so a partial settlement and a split are simply several rows. Credits carry overpayment forward against a nominated invoice.
Expenses carry GST compliance fields and roll into BAS periods, which is the point at which the ledger stops being a list of invoices and starts being accounts.
Every choice on this page points the same direction. GST is computed per line so that a mixed invoice is correct rather than approximately correct. Deletion writes to a log so a removal leaves a trace. Recalculation is guarded so a partial update cannot silently rewrite an issued total, and guarded again so an empty recompute cannot zero a real one. Overdue is derived so it cannot be stale. And the database is a file on a volume you hold, so none of the above is contingent on the vendor continuing to exist.
That last one is what the name local-first is really buying. Not offline. Custody.
The ledger is a file you own.
Same container on your laptop as in production, same schema, same one database.
Brontiq Books is a real product with real customers, so nothing on this page is taken from its data. The screens are drawn from the running product; every client, contact, ABN, bank detail, invoice number, date and figure shown is invented. The arithmetic is genuine: the invoice lines sum to their subtotal, GST is exactly 10 per cent of it, and the statement balance is the difference between what was invoiced and what was paid.