Skip to content

BoardLedger

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.

Version Licence PHP Dependencies

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.

The Bills page


Contents


Why this exists

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.

Features

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

Bills — everything owed this month, recalculated on load
Bills
Rooms — the rent-bearing entity
Rooms
Flatmates — who is in which room, and when
Flatmates
Guests — daily rate, split two ways
Guests
Utility bills — entered once a month
Utility bills
Payments — what has come in
Payments
Two-factor — QR drawn on your own server
Two-factor
Accounts — roles and 2FA status
Accounts
Sign-in and About

Sign in

About

Screenshots show a demonstration database. All names are placeholders.

Requirements

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.

Quick start

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_html

Open 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:

  1. Rooms — add a room with a weekly rent, say Room 1 at $280.
  2. 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.
  3. Utility bills — add Power for whatever the last one was.
  4. 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.

Installation

Shared hosting (the intended case)

  1. Download the release archive, or build one with php bin/package.php.
  2. Upload the contents of public_html/ into your hosting account's public_html directory. That is the whole installation.
  3. Make data/ writable by the web server — 755 is usually enough, 775 if the host insists. If it is not, the application says so plainly and names the permissions to set rather than returning a blank page.
  4. Visit the site. The database is created automatically and you are asked to create the administrator account.

Upgrading

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.

Before putting this on the public internet

  • 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.sqlite returns 403 over HTTP. It is protected by .htaccess; on a host that ignores AllowOverride it would otherwise be downloadable. If you are on nginx or Caddy, deny /data, /src, /views and /config in your server configuration — those directories ship with Apache rules that other servers do not read.

Configuration

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     = 900

TOML 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.

How the billing works

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.

Room rent

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.

Guests

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.

Utility bills

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

Rounding

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.

Database

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_by and deleted_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.

Building from source

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.php

The 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.

Tests

for t in tests/*_test.php; do php "$t"; done
billing_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.

Documentation

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

Roadmap

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 occupancies table, 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.

Contributing

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.

Support

Licence

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.

Credits

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.

About

Monthly rent and utility billing for flatmates and boarding houses. Built with plain PHP and SQLite, with zero dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages