How to Set Up an Invoicing and Bills App Using Only Your Browser

Invoices out, bills in, one screen that says what is due this week — deployed from a GitHub fork to a container with a managed PostgreSQL database, from the first click to backup, restore and updates, without opening a terminal.

An invoicing app screen titled This week, October 12 to 19, 2026: tiles for money due in $858.60, seven overdue invoices $5,326.50, money due out $328.00, three overdue bills $1,020.00 and $8,575.40 received in the last seven days, above two tables of invoices and bills due this week

If you run a small business, invoices go out, bills come in, and the question on Monday morning is always the same: what is due this week, and who still owes me? A spreadsheet answers it until the third person edits it. Invoicing services answer it for a monthly fee and keep your data on their side.

This post is the third option: an app that is yours, on hosting you pay US$10 a month for, set up using only your browser. You fork a repository, create a container with a database switched on, open the URL and start. No terminal, no code editor, no installing anything on your computer.

Try it, deploy it, read it: the live demo is at firmbookenvtest-07gb19sd.b4a.run — sign in with [email protected] / acme-demo-2026, read only, fictitious data. The template is at github.com/templates-back4app/firmbook.

Everything below was done on October 9–12, 2026, on the Back4app Containers platform, and every number comes from those runs. The logs are in the repository under docs/notes/ and docs/evidence/.

Who this is for, and what the app does

The reader we had in mind runs a shop, a studio or a practice, issues a few dozen invoices a month, and can follow instructions on a web page. The app is deliberately small so that it can be understood in an afternoon:

  • Contacts — customers and suppliers in one list, with the address printed on invoices.
  • Invoices — line items, quantity, unit price, a tax rate that is either added to prices or extracted from them, sequential numbers per year (INV-2026-0001), a printable page and a share link.
  • Payments — money received, applied to one invoice or kept as credit; partial payments, overpayments and refunds handled honestly (more on that below).
  • Bills — what you owe, with due dates, categories and payments.
  • This week — money due in and out in the next seven days, what is overdue, what came in and went out in the last seven.
  • Settings — company details, tax, people (owner, staff, read-only demo), sample data, backup, restore, CSV export, and a log of who did what.

It is not accounting software. There is one currency, no double-entry ledger, no bank feed, no tax filing. When you outgrow it, the data leaves in one file.

Firmbook's This week screen: tiles for money due in and out, overdue totals and cash received, above two tables of invoices and bills due this week

The stack, and why it is this one

For a reader who will never open the code, we wanted the fewest moving parts that still survive real use: one Node.js process (Express, server-rendered pages, no JavaScript framework to build), one PostgreSQL database (the add-on Back4app Containers offers in the create form), and a GitHub repository the reader owns.

Back4app builds it with the Node.js Buildpack — the dashboard reads package.json, sees "engines": { "node": ">=22" } and the start script, and builds the image. There is no Dockerfile in the repository, and nothing to configure under Build & run.

Two platform details decided the design, and both are worth knowing before you click:

Deploying it, click by click

1. Fork the template. On github.com/templates-back4app/firmbook click Fork. Not Use this template: a fork keeps a link to the original, which is what lets GitHub’s Sync fork button bring you updates later.

A fork of a public repository is public. The repository holds no business data, and the table further down says where every private thing lives.

2. Create the app. In the Back4app dashboard open Containers → Deploy a web app, choose GitHub repository and select your fork. The form detects Node.js Buildpack · Node.js >=22, port 8080 and the start command; the health check is /healthz.

3. Switch on the database and type the passphrase. In the same form, under Database, switch on PostgreSQL · 1 GB; the plan changes to Starter because the free plan cannot have a database. Under Environment variables add SETUP_SECRET with a passphrase of at least five unrelated words you make up, or a value from your password manager. Click Create app.

4. Open the URL as soon as it is green. The first build took 147 seconds in our run (there is no cache yet). The app’s first-run page asks for the passphrase and creates the owner account — or restores a backup from another deployment. It shows eight one-time recovery codes: save them.

The first-run page of a fresh deployment: create the owner account with the setup passphrase, or restore a backup file

5. Settings, then sample data. Fill in company name, address, tax label and rate, and whether the prices you type include tax. Click Add sample data to see the app with 24 invoices, payments and bills; click Remove sample data when you start for real — it deletes only the rows it tagged, and refuses if any real record points at a sample one.

Why the passphrase at all? Because an unpublished URL is not a secret: new hostnames appear in public certificate logs within minutes, and the first visitor to an empty app would otherwise become its owner. The passphrase is consumed in the same database transaction that creates the owner, so two simultaneous attempts cannot both succeed — we fired ten at once and got one owner and nine refusals.

Create a contact, then an invoice: description, quantity, unit price per line, due date (blank means 14 days after issue). Prices are tax-exclusive by default; with the prices include tax setting the app stores net = round(gross ÷ (1 + rate)) per line, so the customer’s total is exactly what you typed. Three lines at $19.99 including 10 % tax come to $59.97 with $5.46 of tax.

Issue freezes the invoice: it gets the next number of the year inside the same transaction (ten invoices issued simultaneously got ten consecutive numbers), and a snapshot of the contact’s name and address, so later edits to the contact never change an issued invoice. An issued invoice is never edited; it is voided and a new one issued.

An issued invoice: company header, bill-to block, three lines, subtotal, sales tax and total, with the Record a payment, Mark as sent and Create share link buttons

Record a payment from the invoice page. A payment is a cash movement; what links it to invoices is a separate allocation. That distinction is what keeps the numbers honest: a $120 payment on a $100 invoice marks the invoice paid and leaves $20 of credit on the customer.

Applying that credit to the next invoice creates no new cash, and the Payments list still says $120 received. Voiding an invoice that has payments moves those allocations to the replacement or to credit — the money never disappears from a report.

Every form carries a one-time key, so a double click or a retried submit records nothing twice (five simultaneous submits of one payment form produced one payment).

Share link. Each issued invoice can get a link with a 32-byte random token. The page shows the invoice and a print button, nothing else; it sends X-Robots-Tag: noindex, Referrer-Policy: no-referrer and Cache-Control: no-store, loads no third-party assets, and dies when you revoke it.

There is no e-mail sending in this release: you paste the link or print to PDF, then click Mark as sent.

The public share page of an invoice: the invoice sheet and a Print / save as PDF button, no navigation

What you own, and where it lives

Everything the business depends on is either in your fork or in your database, and both leave with you: the fork by cloning, the database as one backup file or a CSV export. The table says where each thing lives and who can read it.

WhatWhereWho can read it
Your fork of the codeGitHub, publiceveryone — it holds no business data
Company details, tax, invoice footerthe app’s database (Settings)people you add to the app
Logo, colors, labelscustom/ in your fork, publiceveryone
Contacts, invoices, payments, billsPostgreSQL on Back4appthe app; the dashboard’s table browser; your backup file
Passwordshashed (scrypt) in the databasenobody — a reset needs a recovery code or the dashboard
Sessions, one-time keysthe database, never exportedthe app

The dashboard’s Database page is a real table browser: tables, columns, indexes, a SQL console and a CSV export of the page you are looking at. What it does not have, as of October 2026, is a backup or snapshot button for the add-on. Your recovery path is the app’s own backup file.

Backup and restore, timed

Settings → Download backup produces one zip with every table as JSON, taken in a single repeatable-read transaction (we took one while eight payments were being recorded in parallel; every allocation in the file pointed at a payment in the same file). Sessions, one-time keys and reset tokens are never included. Treat the file like a bank statement.

Restore lives in two places: on the first-run page of an empty deployment, and under Settings for the owner, where it replaces everything and signs everyone out. A backup made by a newer release than the app you restore into is refused, with the release to install first.

On the live app: a backup with 1,024 invoices, 3,072 lines and 2,024 payments was 155,388 bytes and took 2.2 s; restoring the 248-row file from before that run took 714 ms.

Settings: the Backup and restore section with Download backup, Export CSVs and the Restore form

Updates, and what “back” means

Updating is Settings → Download backup, then Sync fork → Update branch on your fork’s GitHub page, then waiting for the automatic deploy (68 s from push to a new version answering, in our runs — switch Autodeploy on under Build & deploy; an app created on the free plan has it off).

Your customizations live in custom/ — logo, theme, labels — a folder a release never touches, so they cannot conflict with an update. If you edited any other file, GitHub may report a conflict and offer its web editor; beyond simple cases that is a developer’s job, and the article says so rather than pretending otherwise.

If the new release fails to start, the app shows a plain temporarily unavailable page — no error text, no logs — and writes the reason to the runtime log you read in the dashboard. All of a release’s migrations run in one transaction, so a failed one leaves the database exactly as it was.

Instant Rollback in the dashboard then starts the previous image again; the dialog says, in its own words, that the database is not rolled back. We measured 7 s to roll back and 3 s to roll forward.

Lost password, lost everything

A recovery code resets the owner’s password and signs out every other session. If the codes are lost too, the login page’s Lost everything? screen shows a fresh random code and asks you to paste it into the dashboard as OWNER_RESET_TOKEN and deploy.

The app accepts only a code that arrives through the environment, once, within 30 minutes of that deploy. Knowing the owner’s e-mail is worthless on its own — that is a test in the repository, not a promise.

One caveat follows from the finding above: this flow depends on a variable added after creation reaching the container, which failed on one of our two apps. Until that is resolved on the platform side, the recovery codes are the path to rely on.

Scale, latency and size

We filled the live app through its own forms — 1,000 invoices with three lines each and 2,000 payments, six in parallel — in 605 seconds with zero failures. Then we measured page latency from a single client outside the US, 100 sequential requests per page after five warm-ups, signed in as the demo user:

Measured · October 12, 2026 · 1,024 invoices, 3,072 lines, 2,024 payments · 100 requests per page, one client outside the US
1.27 sinvoices list, p50, all rows on one page (0.1.0)
→
632 msinvoices list, p50, 100 rows per page (0.1.1)
510 msThis week, p50 (p95 615 ms)
Database size · pg_database_size, same dataset
9.5 MBwith the sample data, 24 invoices
→
12.6 MBwith 1,024 invoices
≈3 KBper invoice with lines and two payments

The lists were the one thing that did not survive the dataset as first written: the invoices page rendered all 1,024 rows at once and took 1.27 s. Release 0.1.1 paginates at 100 rows, and the number above is from the second run. Storage is not the limit a small business hits: about 3 KB per invoice with its lines and two payments means the 1 GB pack holds on the order of 300,000 invoices.

The connection limit is. The add-on’s database role accepted 15 extra connections on top of the one the app held and then answered too many connections for role — about 16 per app. The template’s pool is capped at 5; a pool sized for a bigger database would make this app fall over.

What we found while building it

  • Autodeploy is off on an app created on the free plan, and upgrading does not switch it on. Build & deploy → Autodeploy → Save, and the dialog offers to deploy at once.
  • A plan change redeploys the app, and the edge answered bad gateway for about a minute while the new container came up. The reactivated database came back in place, with its data.
  • A canceled plan blocks the database rather than deleting it: the Database page says the data is kept for seven days and comes back on the next deploy when the plan is reactivated. We saw it after a payment hiccup.
  • The dashboard read our .env.example: a Finish your setup dialog listed SETUP_SECRET and the start command it had detected, and asked for the value. A good place to type it — on the app where variables worked.
  • Lists need pagination before 1,000 rows, see above.
  • GitHub’s secret-scanning push protection is off on a public repository by default and a fork does not inherit it. We turned it on for the template; your fork is yours to switch on, and the rule stands regardless: nothing that is a password or a customer’s detail goes into a file.

When not to use this

Use QuickBooks, Wave or Xero when you need payroll, bank feeds, tax filing or an accountant with their own login. Use a spreadsheet when you issue a handful of invoices a year.

Use a developer-facing starter — our cash flow tracker is one — when you want a backend-as-a-service with class permissions and Cloud functions rather than one process and one database.

And note what this post did not test: a non-technical person following only this text, from fork to restore. The step timings are the platform’s; the human part is not timed yet. When we run that test, this paragraph gets replaced by its result.

The repository

github.com/templates-back4app/firmbook — MIT. README.md is the deploy guide; docs/guides/ has backup and restore, updates, what to do when something breaks, safe customization with an AI assistant (a prompt that is allowed to touch only custom/), and attaching your own domain.

test/run-all.sh runs the 84 local checks — invoice math, roles, session replay, concurrent setup, concurrent issuing, double submits, consistent backup under writes, restore into a fresh app, a release whose second migration fails — and test/live-check.mjs the 18 we ran against the deployed app. docs/notes/gate-1-platform.md is the platform diary behind every number here.

Tagsinvoicingsmall-businesspostgresqlcontainersbuildpacksnodeexpressno-terminalbackup

Frequently asked questions

Do I need to know how to code to set this up?

No, and you do not need a terminal either. Every step is a page in a browser: fork the repository on GitHub, create the app in the Back4app dashboard with the PostgreSQL add-on switched on, type one passphrase, open the URL and create the owner account. Changing how the app calculates or stores data is a different matter — that needs a developer.

What does it cost to run?

As listed in the Back4app dashboard in October 2026: the Starter container is US$5 a month and the PostgreSQL pack (1 GB) is US$5 a month, billed pro rata — US$10 a month together. The free plan cannot have a database. A custom domain, if you want one, is bought from a registrar separately.

Is my data safe if the hosting goes away?

The app has a Backup button that downloads one file with everything — contacts, invoices, payments, bills, settings, people — and a Restore page that rebuilds a fresh deployment from that file. We timed both on the live app: a backup with 1,024 invoices took 2.2 seconds and a restore 0.7 seconds. The database itself is standard PostgreSQL, and there is also a CSV export for spreadsheets.

Can a customer open the invoice link without an account?

Yes. Each issued invoice gets a share link with a 32-byte random token. The page shows that invoice and nothing else, is marked not to be indexed by search engines, sends no referrer, and can be revoked from the invoice. Google Safe Browsing reported no issue for our app's hostname; one security tool on one of our machines flagged all *.b4a.run hosts, which is why the guide includes attaching your own domain.

What happens when I install a template update and it breaks?

Every migration in a release runs in one database transaction, so a failed update applies nothing and the app shows a plain 'temporarily unavailable' page. In the Back4app dashboard, Runtime logs show the reason and Instant Rollback brings the previous version back — 3 to 7 seconds in our runs — without touching the database.

Is this accounting software?

No. It tracks invoices, payments, bills and what is due, in one currency, with no double-entry ledger, no bank feeds and no tax filing. If you need payroll, bank reconciliation or an accountant's access, QuickBooks, Wave or Xero are the right tools; this app is for the business that needs invoices and bills under control and wants to own the data.

Further reading

Reviewed & verified by Back4app Engineering, Editorial review at Back4app

Tested Oct 12, 2026 — Containers: Back4app, Starter plan, 0.5 vCPU, 512 MB, PostgreSQL add-on 1 GB (PostgreSQL 18.3) · Template: Node 22, Express 5, node-postgres 8.

More from Back4app Engineering

Containers · tutorial

How to Build a Cash Flow Tracker With JavaScript

AI Agent · tutorial

How to Turn a Spreadsheet Into a Web App With a Database and Login

Containers · error-guide

Why Your App Works Locally and Crashes When You Deploy It [Full Repo]