# Household Budgeting App

> My family keeps money in cash, mobile wallets, banks and dollar accounts, and several of us spend from the same pot. No app saw all of it, so I built the view none of them had.

A case study by Arif Hossen  
Source: https://arifhossen.dev/work/household-budgeting-app

- **Role:** Sole designer and engineer
- **Team:** Me, with my family as users
- **Duration:** 13 active days
- **Stack:** Laravel 13, Inertia, Vue 3, TypeScript
- **Period:** May to Jun 2026
- **Engagement:** Own project
- **205** Commits, all mine, over 13 active days
- **68** Test files, 351 test blocks by count
- **9** Model policies, plus one for the household
- **8** Decimal places stored on every exchange rate

## No app saw all of it

Context

A household in Bangladesh keeps money in cash, in mobile financial services, in two or three banks and in dollar wallets used for work abroad. Several people spend from the same pot, lend to relatives and pay recurring bills. No bank app sees all of it, and a spreadsheet cannot be handed to a parent who reads only Bengali.

I wrote the requirements down first, from my own family's use. Then I wrote an engineering specification that supersedes them wherever the two disagree. After one round of questions to the family, multiple users and multiple currencies moved from out of scope into version one. That was the most useful thing the process caught, and it came before any code.

## Whole numbers, plus the rate that was used

The money model

Buying dollars to pay for something abroad is not spending. It is the same money in a different wallet, minus a fee, at a rate. Most trackers refuse the second currency or convert at whatever rate is current, which quietly rewrites the past. I wanted neither, and I had no rates feed to lean on.

Every amount is stored as whole minor units with its own currency, so there are no decimal fractions anywhere in the schema. A cross-currency move is one transfer record carrying both amounts, both currencies, the rate and an optional fee with the wallet the fee came from. It is not two entries that lost the rate between them.

| Stored on every transaction | What it buys |
| --- | --- |
| Amount in minor units, with its currency | No fractions and no floating-point error |
| The exchange rate used, to eight decimals | The past is never re-priced |
| The base-currency amount at the time | A mixed-currency month sums in one query |
| The fee, and the wallet it came from | A transfer is one record, not two |

A dollar payment from March still reports the same taka figure today, because nothing is re-derived.

## Another household's data does not exist

Tenancy

One person can belong to several households, their own and a parent's, and switch between them. That is the shape in which a single forgotten condition in one controller shows one family another family's money. So the scope lives on the model, not in the controller.

A shared trait puts a global scope on every household-owned model, limiting reads to the current household, and a creating hook stamps the household on every write. Validation resolves ids through the same scope, so another household's wallet does not come back forbidden. It comes back as not existing. A forbidden response would still confirm the record is real. A middleware makes sure a household is selected before any scoped screen loads.

<!-- diagram:household_tenancy -->
**Diagram:** Household tenancy in three layers

*The scope is on the model, not in the controller. A request for another household's record fails validation as not found before any policy is consulted.*

A leak would need the read scope, the write hook and the policies all to fail together. The isolation has its own tests.

## Three roles, and a trail for every change

Roles and history

Each member is an owner, an editor or a viewer, enforced by policies on every model rather than by hiding buttons. A viewer's write request is refused on the server. Household administration is split into separate abilities: viewing, inviting, changing a role, removing a member and deleting the household are five permissions, not one admin flag.

A model observer writes an audit row for every create, update and delete, with the before and after values and the person responsible. Deleting that person's account leaves the log intact. Wallets and counterparties that transactions refer to cannot be deleted at all. Merging two duplicate counterparties repoints their history to the survivor and leaves the absorbed record behind as a tombstone that says where it went.

## Rent that posts once

Automation

The entries people stop recording first are the ones that repeat: salary, rent, internet, instalments. Templates for those post themselves just after midnight. A job that creates financial rows can create them twice, and double-posted rent is worse than no automation, so I closed that off in three places.

| Guard | What it prevents |
| --- | --- |
| Each template runs in its own transaction, then advances its last-run date | A second run on the same day does nothing |
| The schedule is declared non-overlapping | A catch-up run cannot collide with the nightly one |
| The schedule is declared single-server | Two machines cannot both post the same entry |

The day of the month is capped at 28 so that every month can satisfy it. A date override exists so a missed night can be caught up on purpose.

## Bengali first, down to the digits

Language

The people this is for read Bengali and type amounts in Bengali numerals. Most software treats that as a display setting, translates the labels and keeps the input in Western digits. A parent types a Bengali numeral into an amount field and is told they typed nothing.

Bengali is the default language. The language for a request is resolved in a stated order: a request parameter, then the person's preference, then the household's, then Bengali. Numerals typed into amount fields are normalised on input instead of rejected. Category names are stored in both languages on the record. The help centre is written twice, one file per language per chapter. The editor for the public landing page will not save a string in only one language, so the Bengali version cannot quietly rot.

## Limits, stated as decisions

What I do not know

Nothing here connects to a bank or a mobile-money service. Entries are recorded by hand, by design, and the first version's limits are written down as decisions rather than left as surprises.

I have not recorded whether the app is in daily use, how many households, wallets or entries it holds, or whether the nightly job runs on a real server today. This page makes no claim about any of them. It claims only what the repository shows: 205 commits over 13 active days, and a test suite that covers features, policies, browser flows and deployment assumptions.
