Monthly billing for a boarding house. Prorated room rent, guest charges, shared utility bills and payments — worked out to the cent, in a single page you open once a month.
Plain PHP 8.3 and SQLite. No framework, no Composer, no npm, no build step and nothing that phones home. Deployment is copying a directory.
- Why this exists
- Features
- Screenshots
- Requirements
- Quick start
- Installation
- Configuration
- How the billing works
- Database
- Building from source
- Documentation
- Roadmap
- Contributing
- Support
- Security
- Licence
- Credits
A boarding house has one landlord and several people who each owe a different amount every month, because they moved in halfway through it, or shared a room for a fortnight, or a guest stayed for five nights and put something towards the power bill. Working that out in a spreadsheet is possible and takes an evening, and one dragged formula makes somebody's rent wrong without anybody noticing.
BoardLedger replaces that spreadsheet. Rooms, flatmates, guests and bills are entered once; the amount each person owes is recalculated from scratch every time the Bills page is opened. There is no "generate" button to forget to press.
It is built for the way small landlords actually host: FTP access to a shared
hosting account, no shell, no ability to run composer install. Everything it
needs is in the repository, which is why the TOML parser, the QR encoder and the
test harness are all written into the project rather than pulled in.
Billing that reconciles
- Rent belongs to the room, charged weekly, fortnightly or monthly, and converted to a nightly rate.
- Shared rooms split night by night, so somebody alone for half the month pays the full rate for those nights.
- Guests charged a daily rate, optionally split between room income and a contribution towards the month's utility bills.
- Utility bills divided by occupancy days, after guest contributions have come off the whole pot in proportion.
- Rounding that always adds back to the whole, spreading leftover cents one at a time so no one person visibly pays an odd figure.
- A reconciliation line on every Bills page, so the arithmetic can be watched to balance.
Accounts and access
- First-run setup creates one administrator, then closes permanently.
- Three roles — administrator, user, read only — checked on every action, not merely hidden in the interface.
- Account lockout, idle and absolute session timeouts, and remember-me with rotating tokens.
- Two-factor authentication (TOTP, RFC 6238) with the QR code drawn on your own server, so the secret never reaches an outside service.
Built to last
- Every record carries a UUID version 7, generated once and never changed. Integer ids are never exposed.
- Soft deletion, so a bill already shown to a flatmate never loses the room it referred to.
- The database checks its own shape on every connect and stops with the missing column named rather than failing later.
- Upgrades are a file copy; databases migrate themselves on the first request.
- 615 assertions across six test suites, including both worked examples from this README reproduced to the cent.
Screenshots show a demonstration database. All names are placeholders.
| PHP | 8.3 or newer |
| Extensions | pdo_sqlite, mbstring, json, session |
| Web server | Anything that serves PHP — Apache, nginx, Caddy, or PHP's built-in server |
| Database | None to install. SQLite lives in a single file, created on first use |
| Build tooling | None |
No MySQL, no MariaDB, no Composer, no npm, nothing installed globally. The zip
extension is needed only by bin/package.php, which runs on a developer machine
and never on the server.
Try it locally in under a minute:
git clone https://github.com/ajsap/boardledger.git
cd boardledger
php -S 127.0.0.1:8000 -t public_htmlOpen http://127.0.0.1:8000. You will be asked to create an administrator account, and then you are in.
To see something happen, in this order:
- Rooms — add a room with a weekly rent, say
Room 1at$280. - Flatmates — add somebody to it, with a move-in date earlier this month. Add a second person to the same room with a move-in date halfway through.
- Utility bills — add
Powerfor whatever the last one was. - Bills — open it. Nothing to press.
The second flatmate pays half rate only for the nights they were actually there, the first pays the full rate for the nights they had the room alone, and the power bill is split by the days each of them occupied the property. The reconciliation line at the bottom shows it balancing.
- Download the release archive, or build one with
php bin/package.php. - Upload the contents of
public_html/into your hosting account'spublic_htmldirectory. That is the whole installation. - Make
data/writable by the web server —755is usually enough,775if the host insists. If it is not, the application says so plainly and names the permissions to set rather than returning a blank page. - Visit the site. The database is created automatically and you are asked to create the administrator account.
Replace the files. Databases from earlier releases migrate themselves on the first request, and the application verifies its own schema on every connect — if a column is ever missing it stops and names it rather than failing later.
Take a copy of data/boardledger.sqlite first, as you would with anything.
- Serve it over HTTPS. Session and remember-me cookies are marked secure only when the request arrives over HTTPS.
- Turn on two-factor authentication for every account that can change data.
- Confirm
data/boardledger.sqlitereturns 403 over HTTP. It is protected by.htaccess; on a host that ignoresAllowOverrideit would otherwise be downloadable. If you are on nginx or Caddy, deny/data,/src,/viewsand/configin your server configuration — those directories ship with Apache rules that other servers do not read.
Application settings live in public_html/config/config.toml — a plain file you
can read, comment and keep under version control. Business data stays in SQLite.
[app]
name = "BoardLedger"
environment = "production"
timezone = "Pacific/Auckland"
currency = "$"
date_format = "j M Y"
[session]
idle_timeout = 1800 # seconds of inactivity before signing out
absolute_timeout = 43200 # seconds since signing in, however busy
remember_days = 30
[security]
password_min_length = 10
max_failed_logins = 5
lockout_seconds = 900TOML was chosen because it takes comments, is strongly typed, and cannot be broken by an errant space the way YAML can. The parser is part of the project.
If the file is missing or malformed the application keeps running on built-in defaults and logs the problem, so a typo cannot lock you out of the interface you would use to fix it.
Not every key is wired up yet. Eighteen of the thirty-three settings are currently declarative. docs/configuration.md lists every key and states plainly whether it does anything.
Every day count includes both the first and the last day. Somebody who moves in on the 5th and out on the 9th occupied the property for five days.
Rent belongs to the room, not to a person. Whatever the frequency it converts to a nightly rate — divided by 7, by 14, or by the number of days in the month being billed, so a monthly agreement charges exactly the agreed figure in February and July alike.
Where a room holds more than one flatmate, each night's rent is divided between whoever was in that room that night.
| Room at $280/week, 31-day month | $40.00 a night |
| Nights 1–15, one occupant | 15 × $40.00 = $600.00 |
| Nights 16–31, two occupants | 16 × $20.00 = $320.00 each |
| First occupant pays | $920.00 |
| Second occupant pays | $320.00 |
| Room collects | $1,240.00 — its full month |
Splitting by total days instead would charge $876 and $364, giving the first occupant a discount for the fortnight they had the room to themselves. The room collects the same either way; only the fairness differs.
A guest is charged days × daily rate, and that rate can be split. At $80 a day with $30 set aside for utilities, $50 a day is room income for the landlord and $30 a day comes off the month's utility bills before they are shared — so everybody benefits from the room having been let.
Guests never change what flatmates pay for their room.
Entered once a month. Guest contributions come off the whole pot first, reducing every bill in proportion, and what remains is divided between the flatmates by the days each of them occupied the property.
With bills of $608.28, a $150 guest contribution, and flatmates present for 31, 16 and 31 days:
Bill total $608.28
Less guest contributions −$150.00
Shared between flatmates $458.28
31/78 → $182.14 16/78 → $94.01 31/78 → $182.13
The parts always add back to the whole, and leftover cents are handed out one at a time rather than dumped on one person — six people sharing $100 pay $16.67 four times and $16.66 twice, never $16.67 five times and $16.65 once.
Both worked examples above are reproduced assertion by assertion in
tests/billing_test.php. If either ever stops matching, the tests fail.
One SQLite file — public_html/data/boardledger.sqlite — holding everything.
Back it up by copying that file.
- Every record carries a UUID version 7, generated on creation and never changed. It is what appears in URLs and forms and what relationships point at; sequential integers are never exposed.
- Version 7 rather than version 4 because the first 48 bits are a millisecond timestamp, so identifiers sort by creation time and append to the index instead of scattering random inserts across it.
- Foreign keys reference the parent's UUID, so a table exported on its own is still meaningful and two databases can be merged without renumbering.
- Every table carries
created_at,updated_at,created_by,updated_byanddeleted_at. Deletion is soft: the row stays, marked, so anything that referred to it keeps working. - Usernames are labels rather than identity. They can be changed at any time and existing sessions survive, precisely because nothing in the database refers to a username.
The full schema, every column and the migration chain are documented in docs/database.md.
There is nothing to compile. "Building" means running the tests and producing the upload archive.
# run the test suites
for t in tests/*_test.php; do php "$t" || exit 1; done
# build dist/boardledger-<version>.zip
php bin/package.phpThe packaging script reads the version from public_html/config/version.php,
refuses to build if any shipped PHP file fails to parse, and refuses to build if
the version number appears anywhere outside that one file. The archive contains
the contents of public_html/, so it unzips straight into a hosting account.
for t in tests/*_test.php; do php "$t"; donebilling_test.php 23 groups, 182 assertions, all passed
qr_test.php 10 groups, 164 assertions, all passed
schema_test.php 7 groups, 81 assertions, all passed
toml_test.php 11 groups, 98 assertions, all passed
totp_test.php 10 groups, 61 assertions, all passed
uuid_test.php 8 groups, 29 assertions, all passed
Six suites covering the billing arithmetic, database upgrade paths, the TOML
parser, TOTP against the RFC 6238 reference vectors, the QR encoder against the
published generator tables, and UUID generation. No PHPUnit — the project takes
no Composer dependencies, so the harness is tests/harness.php, about 200 lines.
| Document | What is in it |
|---|---|
| docs/architecture.md | Request flow, routing, the layers, and why each decision was made |
| docs/database.md | Every table and column, the identity model, migrations |
| docs/configuration.md | Every configuration key, and whether it currently does anything |
| ROADMAP.md | What is planned, in priority order |
| CHANGELOG.md | What changed, and when |
The next body of work, in priority order. See ROADMAP.md for the full list.
- Payments must reach the bills. They are recorded but do not yet affect what anybody owes; there is no running balance carried month to month. This is the largest functional gap in the product.
- Interface overhaul — a dashboard landing page, sidebar navigation, modal add and edit, bills grouped per flatmate as cards, an occupancy timeline, global search, keyboard shortcuts and dark mode.
- Reports and invoices — rental income, outstanding balances, occupancy history, and a printable statement per flatmate.
- Backups — manual, scheduled and restore.
- Separate
occupanciestable, so a person can change rooms or leave and return while keeping one identity. - Multi-property support. The identity model was designed for it already.
Contributions are welcome. Please read CONTRIBUTING.md first — it covers the coding standards, the constraints that shape the project (no Composer, no build step, deployment stays a file copy), and how to run the tests.
By taking part you agree to abide by the Code of Conduct.
- Questions and help: see SUPPORT.md.
- Bugs and feature requests: open an issue.
- Security issues: please do not open a public issue. Follow SECURITY.md.
BoardLedger is free software, licensed under the GNU Affero General Public License, version 3 or (at your option) any later version. The full text is in LICENSE.
The AGPL was chosen deliberately. Section 13 means that if you run a modified version of BoardLedger somewhere other people can reach it over a network, those people are entitled to its source. That is the right arrangement for software that handles somebody's rent.
Set APP_SOURCE_URL in public_html/config/version.php to wherever your source
is published if you deploy a modified version; the About page links to it.
Copyright (C) 2026 Andy Saputra
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
Author — Andy Saputra andy@saputra.org
Written in New Zealand. British/New Zealand English throughout, in the code and the documentation.
Everything in this repository was written for it. Nothing is vendored, and there are no third-party dependencies to credit — which is itself the point: the project must remain deployable by copying files to a hosting account with no shell.
The standards it implements are somebody else's good work: RFC 6238 and RFC 4226 for time-based one-time passwords, RFC 4648 for base32, RFC 9562 for UUID version 7, ISO/IEC 18004 for QR codes, TOML for configuration, Semantic Versioning and Keep a Changelog.









