Utility bill tracking for rented housing. Meter readings, tariffs, extra expenses and an exchange rate go in; how much cash the tenant owes this month comes out. One report per month, each with a public link for the tenant.
Renting a flat abroad usually means two currencies: utilities are billed in the local one, rent is agreed in dollars, and the rate moves every month. This app does that arithmetic and keeps a record of it, instead of a spreadsheet that gets rewritten every thirty days.
Built for a single administrator - the landlord. There is no users table; the login and password hash come from the environment.
- A report per month: three meters (electricity, water, gas) with their tariffs, additional expenses, the bank transfer and the rent
- Fetches the exchange rate for a chosen date from a configurable source
- Carries the balance - overpayment or debt - into the next month automatically
- Creating a new month copies the tariffs and seeds the previous readings
- A public link per report, toggled on and off, that a tenant can open without an account
- Russian and English interface, follows the system colour scheme
Next.js 16 (App Router) · React 19 · TypeScript · Prisma 7 · PostgreSQL · NextAuth v4 · Tailwind 4 · shadcn/ui
pnpm install
cp .env.example .env # fill it in, see below
pnpm db:migrate
pnpm devGenerate the administrator password hash (base64 because a bcrypt hash contains
$, which breaks .env parsing):
node -e "console.log(Buffer.from(require('bcryptjs').hashSync('your-password',10)).toString('base64'))"| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
yes | PostgreSQL connection string |
NEXTAUTH_SECRET |
yes | JWT secret (openssl rand -base64 32) |
NEXTAUTH_URL |
yes | Base URL of the app |
ADMIN_USERNAME |
yes | Administrator login |
ADMIN_PASSWORD_HASH_B64 |
yes | base64 of the bcrypt hash |
RATE_PROVIDER |
no | Exchange-rate source, defaults to nbrb |
ALLOWED_ORIGIN |
no | Origin for CORS headers on /api/* |
SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN |
no | Error reporting |
Nothing is hardwired to one country. Both currencies come from the active rate
provider, and so do the labels in the interface. To plug in your own bank, add a
file next to src/lib/rates/nbrb.ts:
export const myBank: RateProvider = {
id: 'my-bank',
name: 'My Bank',
localCurrency: 'XXX', // what utilities are billed in
baseCurrency: 'USD', // what rent is settled in
async fetchRate(date) {
// how much localCurrency one baseCurrency costs; null if there is no quote
},
};Register it in PROVIDERS in src/lib/rates/index.ts and set RATE_PROVIDER
to its id. Nothing else needs to change. The default tariffs in
src/lib/constants.ts are Belarusian - adjust them to your own.
Strings live in src/i18n/dictionaries/. The English dictionary is typed against
the Russian one, so a forgotten key fails the build, and a test checks that
placeholders match. Adding a language is a dictionary file plus a code in
LOCALES.
meter amount = (current - previous) x tariff
local balance = sum of meters - bank transfer - extra expenses
base balance = local balance / rate
cash due = rent + base balance - balance carried from last month
carried on = cash actually paid - cash due
A positive carry-over means the tenant overpaid, a negative one means they owe.
The whole calculation lives in src/lib/calc.ts and is covered
by pnpm test.
The Dockerfile produces a standalone build, and the entrypoint runs
prisma migrate deploy before starting the server, so a failed migration stops
the container rather than serving against an unmigrated database.
docker network create shared # docker-compose.yml expects this network
docker compose up -d --buildThe database is external - point DATABASE_URL at your own PostgreSQL.
- Single user, no roles, no sign-up
- Login rate limiting (3 attempts, 24-hour block) lives in process memory and resets when the container restarts
- The client IP is read from
CF-Connecting-IP/X-Real-IP/X-Forwarded-For; without a trusted proxy in front, those headers can be spoofed - Editing an old report carries the balance one month forward, not down the chain
- NBRB is the only rate source implemented; others are one file each
More detail in docs/architecture.md.
This is a snapshot of a working application, published as is.
- It is not actively maintained. Issues and pull requests are not promised an answer.
- Tests cover the money math, the formatting and dictionary consistency. There is no automated UI coverage - expect rough edges, and own them if you deploy it.
- The defaults - tariffs, the meters themselves - assume a flat with electricity, water and gas on separate meters.
MIT - use it for anything, including commercially, keeping the notice.
Copyright (c) 2026 Vladislav Romanovsky.