
The invoice document showed a total that disagreed with what the payment gateway had been told to charge. Not by much — a few cents on a fractional-quantity line — but the arithmetic lived in two separate files with no shared test and no contract between them. When a client accepted a quote, the number they saw and the number the charge computed from were produced by two independent pipelines that happened to agree on whole quantities and quietly disagreed on fractional ones. On a billing product, that is a trust bug, not a rounding nit. Billora starts there.
Starting point
The build started with a lifecycle question: what does a freelancer or small service business actually need from billing, and where does the category fall short? The research converged on a narrow scope — quote the job, accept online, issue the invoice, collect the payment, know what is overdue. Five steps, fully owned, without accounting, expense management, inventory, or jurisdiction-specific tax filings dragging the product beyond what a business running five to fifty clients a month needs.
Three architectural commitments shaped every subsequent decision. The first: money has exactly one mutation path and no side effects. No trigger touches a financial column, on any of the thirteen project tables. The reasoning is written into the schema itself — the read-modify-write and the duplicated trigger are the canonical double-count bugs in invoicing, and the design removes the possibility rather than testing around it. The second: zero fund custody. The platform holds no money and takes no cut. Charges settle directly in the connected account's balance, not in the platform's. The third: currency and tax configuration are snapshots at document creation. Changing an org's currency setting cannot rewrite existing invoices — a repeating complaint against the category's reference products, treated here as a schema invariant rather than a configuration note.
Reading the market
The invoicing market has a structural property worth naming plainly: the tools that charge nothing on subscription recover their margin at the transaction. Several well-funded incumbents offer unlimited invoices and unlimited clients at no recurring cost, then take a percentage and a per-transaction fee on every payment processed. That model works well for a solo freelancer who wants one platform to manage everything. It works differently for a service business that already maintains its own payment account, or for a buyer who wants to license the billing engine and run it under their own brand.
Connecting your own payment account is not unusual in the category — multiple incumbents support it. What is measurable and rarer is a platform that takes no cut and holds no funds at any point. The charge is created on the connected account's balance, not the platform's. The platform collects no application fee. We verified this by searching for application-fee calls in the entire source: zero hits. That is a claim about architecture, not about price.
The other gap the research documented: invoice and client caps on entry subscriptions, document branding locked behind plan upgrades, and a currency-history bug named as a dealbreaker in enough user reviews to be a category-level pattern. The lifecycle Billora covers is unchanged from what incumbents offer; the constraints it removes are not incidental.
The decisions that shaped it
One writer for money, enforced structurally. The paid amount on an invoice mutates through exactly one database function. Every other occurrence of that column in the application code is a type declaration or a read — confirmed with a search at build close. The status transition and the increment live in the same atomic operation: the invoice status flips to paid when the running total reaches the invoice amount, computed in the same pass that writes the new total. If the invoice row does not match the guard condition, the function raises and rolls back the already-inserted payment record. A payment recorded against an invoice that did not move is exactly the orphan that function exists to prevent. The stated decision: triggers: [] on all thirteen tables, annotated as deliberate — money moves through one auditable path, never through a side effect.
Webhook idempotency as a database constraint, proven against a replay. Stripe documents at-least-once delivery — never exactly-once. The dedup lives in the database: a unique constraint on the provider payment identifier, with a conflict clause that does nothing on repeat and returns the current state rather than fabricating a success. We replayed the same payment identifier against a live database and measured the result: the function returned deduped: true and the paid amount stayed at the original figure, not double it. The constraint exists because the delivery guarantee makes replay a routine operating condition, not an edge case.
Acceptance without a login is a token plus evidence. The end customer in this product is a token recipient, not a user account. No client role exists in the schema. The public acceptance flow runs through server routes that stamp IP address, user agent, and timestamp on acceptance, take a row lock, refuse expired or already-settled quotes, and on re-acceptance replay the original invoice identifier rather than minting a second one. The totals are copied from the quote's snapshot — not recomputed from live data. The migration comment states the reason directly: the quote the customer accepted is the agreement, and the invoice derives from that, current at acceptance, not from any editable table row.
A formatter crashed both document editors while HTTP answered 200. Both the invoice editor and the quote editor initialize currency to an empty string while the org currency resolves asynchronously. A format helper with a default parameter of "USD" fires only on undefined, not on an empty string. Every render before resolution reached the browser's number formatter with a blank currency code, which throws. Every static check passed. The route returned 200. Only the browser caught it. The fix made the formatter total: a malformed code formats as a plain decimal, a well-formed but unknown code falls back to the code name and the number. It deliberately does not guess — a wrong currency symbol on a money figure is a lie where a bare number is merely terse.
Checkout charges the balance due, not the total. A partly-paid invoice has a balance. The charge is computed from that balance, not from the invoice total. The idempotency key on the checkout session includes the charged amount, so a retry of the same request reuses the existing session while a genuinely different balance opens a new one. Overpayment is explicitly allowed and logged; the status flips when the paid amount reaches the total.
What fought back
The arithmetic problem was not solved in one pass. The totals pipeline existed in two places through most of the build: a server-side module and a line-for-line mirror in the editor component, kept identical by nothing. On whole-quantity lines they agreed. On fractional quantities — a 0.5-hour consulting line, a 2.5-unit materials charge — the editor computed in floating-point while the server ran integer cents, rounded per line and per tax rule, with the discount clamped so it could never produce a negative total. The provenance comment in the surviving file names the stakes: the next person to change a rounding point on one side ships a total that disagrees with what the customer is charged. On a billing product that is a trust bug, not a rounding nit.
The fix was extraction. One arithmetic module, client-safe and free of server-only imports so it can be used in the browser without dragging the service client into the bundle, called by both sides with no alternative path. The recurring schedule editor had the same floating-point problem independently and was also fixed by delegation. The two editors were not merged, and the reason is recorded: collapsing them means refactoring the surface that mints invoices on a schedule with no human present, which is high-risk for a defect already closed on the dangerous half.
Honest v1 limits. Queue and Stripe Connect credentials were not supplied during this build. Every path that requires them fails gracefully and transparently — scheduling succeeds, PDF generation returns a pending status, and the Connect authorization endpoint returns a clear configuration error rather than a 500. They are unexercised paths in this deployment, documented as operator action items. Fifteen API-layer contract actions are not yet wired through the user interface. The most buyer-visible: an org owner has no in-product path to manage team members in v1.
What shipped
Billora ships the billing lifecycle: quotes with public online acceptance, quote-to-invoice conversion with line items, discounts, org-configurable tax rules, and sequential numbering; client records with derived statement views showing billed, paid, outstanding, and overdue; manual and online payment tracking; recurring schedules with database-constraint deduplication; overdue reminders; asynchronously generated branded PDFs; and billed and collected reports. Multi-tenant with full isolation, four roles, an admin plane, and feature-gated plans. Verified end-to-end against a live database, including replay scenarios for acceptance idempotency, partial payment, settle, and schedule deduplication.
The invoicing tools that are free are free because the payment is the product. Billora takes no cut, holds no funds, and ships as a codebase the buyer deploys under their own brand.
