A self-hosted personal and small-business finance ledger. Reads your bank statements, classifies the spending, and does Schedule C — your data never leaves your deployment.
1
stars
44
commits
TypeScript
primary language
Sep 1, 2026
updated
A finance ledger you host yourself. Upload bank and card statements, or connect an account, and it works out where the money went — for a household, or for a sole proprietorship that has to answer to a tax return.
Single user, one database, nothing leaving your deployment. No account to make, no subscription, no company between you and your own transactions.

Every figure in these screenshots is from a synthetic household — see
docs/screenshots. No real statement data appears anywhere
in this repository.
Most of this code is not about showing you a number. It is about the number being right when nothing looks wrong.
A ledger fails quietly. It does not crash — it reports $5,000 of spending in a
month that really cost $16,600, because two payment rails were flagged as
transfers and $6,000 paid to a contractor stopped counting. It tells a family of
four they overspend on everything, because nobody asked how many people live
there. It files a $1,430 card payment as fuel because the pattern mobil also
sits inside MOBILE PMT.
Every one of those happened here, and each one is now an invariant with a test and a paragraph in DESIGN.md naming the bug that produced it. That is the actual design of this project: owner's draw is not an expense, an account with no balance is unknown rather than zero, mileage is rated by the day it was driven because the IRS revises the rate mid-year. The features are ordinary. The care about which figure is wrong, and in which direction, is not.
The same ledger asking a different question — what the profit is, and what is deductible. Its own chart of accounts, and its own reports.
Revenue $48,200.00
Cost of sales $11,340.00
Gross profit $36,860.00 76% margin
Operating $14,905.00
Net profit $21,955.00
Set aside $7,591.00 $3,102 self-employment + $4,489 income tax
There is good self-hosted finance software already — Actual Budget and Firefly III are both mature, and if envelope budgeting or a long-established tool is what you want, start there.
What this one leans on:
What it is not: a shared budget for a couple, a mobile app, or a service. It is one deployment, one password, and your own Postgres.
Just want it running? Docker Compose brings up the app and a Postgres beside it:
git clone https://github.com/aroesec/loot.git
cd loot
cp .env.example .env # then set the values docs/deploy.md lists
docker compose up -d
docs/deploy.md covers the rest, including the migrations. To work on it instead of just running it, you need Node 22 or newer, pnpm, and a Postgres database.
git clone https://github.com/aroesec/loot.git
cd loot
pnpm install
cp .env.example .env.local
Three variables are required:
DATABASE_URL="postgres://user:pass@host:5432/loot"
APP_PASSWORD="something long"
SESSION_SECRET="at least 32 characters"
Then:
pnpm db:migrate && pnpm db:seed
pnpm dev
Sign in at http://localhost:3000 with APP_PASSWORD. First run asks whether
the ledger is personal or a business — it picks the chart of accounts, and is
switchable later. Upload a CSV and it starts classifying.
Everything else is optional and the app is fully usable without it: no API key means the classifier runs on rules alone, and no Plaid credentials means you upload statements by hand.
| Optional | Set |
|---|---|
| Model-backed classification and PDF statements | AI_API_KEY, AI_PROVIDER, AI_MODEL |
| Bank syncing | PLAID_CLIENT_ID, PLAID_SECRET, PLAID_ENV, PLAID_TOKEN_KEY |
| Push alerts | VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY |
| SMS alerts | TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM, ALERT_PHONE |
| Scheduled sync | CRON_SECRET |
.env.example documents all of them.
Any Node host and any Postgres. Docker Compose and Vercel are both covered in docs/deploy.md.
A prebuilt image is published to GitHub Container Registry on every push to
main and every release tag:
docker pull ghcr.io/aroesec/loot:0.3.0
Note that docker-compose.yml builds from source rather than using it, so the
image is for your own compose file or orchestrator. Pin to 0.3.0 or a sha-
tag for anything you care about: latest moves, and registry tags carry no v
prefix even though the git tags do.
| deploy.md | Hosting, Docker, migrations |
| security.md | Authentication, rate limiting, proxies |
| classification.md | How transactions get categorized |
| backups.md | Taking and restoring dumps |
| notifications.md | Push, SMS, installing on iOS |
| plaid.md | Bank syncing |
| ai.md | Model providers, including local ones |
| extending.md | Adding sources, providers and channels |
| AGENTS.md | Conventions and invariants, for humans and coding agents |
| DESIGN.md | Why the design is the way it is |
| TESTING.md | What is worth testing here, and what is not |
| CHANGELOG.md | What changed in each release |
pnpm dev build test typecheck | the usual |
pnpm auth:hash '<password>' | generate APP_PASSWORD_HASH |
pnpm db:migrate then db:seed | schema, then the chart of accounts |
pnpm db:reclassify then db:reconcile-debt | re-file history; always in that order, or real spending disappears |
pnpm db:backup | verified pg_dump into backups/ |
pnpm db:audit-income db:audit-period db:audit-splits | prove the totals from outside the code |
package.json is the source of truth for the rest.
Next.js 15 with the App Router, Postgres via Drizzle, Tailwind. The Anthropic and Plaid SDKs sit behind interfaces and are optional at runtime, so neither is required to run the app.
CONTRIBUTING.md covers setup and the invariants worth knowing before changing anything. DESIGN.md explains why the design is the way it is, usually by naming the bug that caused it.
Security reports go through private advisories rather than public issues. See SECURITY.md.
MIT. See LICENSE.
40 commits
4 commits
TypeScript
98.8%
A self-hosted personal and small-business finance ledger. Reads your bank statements, classifies the spending, and does Schedule C — your data never leaves your deployment.
1
stars
44
commits
TypeScript
primary language
Sep 1, 2026
updated
A finance ledger you host yourself. Upload bank and card statements, or connect an account, and it works out where the money went — for a household, or for a sole proprietorship that has to answer to a tax return.
Single user, one database, nothing leaving your deployment. No account to make, no subscription, no company between you and your own transactions.

Every figure in these screenshots is from a synthetic household — see
docs/screenshots. No real statement data appears anywhere
in this repository.
Most of this code is not about showing you a number. It is about the number being right when nothing looks wrong.
A ledger fails quietly. It does not crash — it reports $5,000 of spending in a
month that really cost $16,600, because two payment rails were flagged as
transfers and $6,000 paid to a contractor stopped counting. It tells a family of
four they overspend on everything, because nobody asked how many people live
there. It files a $1,430 card payment as fuel because the pattern mobil also
sits inside MOBILE PMT.
Every one of those happened here, and each one is now an invariant with a test and a paragraph in DESIGN.md naming the bug that produced it. That is the actual design of this project: owner's draw is not an expense, an account with no balance is unknown rather than zero, mileage is rated by the day it was driven because the IRS revises the rate mid-year. The features are ordinary. The care about which figure is wrong, and in which direction, is not.
The same ledger asking a different question — what the profit is, and what is deductible. Its own chart of accounts, and its own reports.
Revenue $48,200.00
Cost of sales $11,340.00
Gross profit $36,860.00 76% margin
Operating $14,905.00
Net profit $21,955.00
Set aside $7,591.00 $3,102 self-employment + $4,489 income tax
There is good self-hosted finance software already — Actual Budget and Firefly III are both mature, and if envelope budgeting or a long-established tool is what you want, start there.
What this one leans on:
What it is not: a shared budget for a couple, a mobile app, or a service. It is one deployment, one password, and your own Postgres.
Just want it running? Docker Compose brings up the app and a Postgres beside it:
git clone https://github.com/aroesec/loot.git
cd loot
cp .env.example .env # then set the values docs/deploy.md lists
docker compose up -d
docs/deploy.md covers the rest, including the migrations. To work on it instead of just running it, you need Node 22 or newer, pnpm, and a Postgres database.
git clone https://github.com/aroesec/loot.git
cd loot
pnpm install
cp .env.example .env.local
Three variables are required:
DATABASE_URL="postgres://user:pass@host:5432/loot"
APP_PASSWORD="something long"
SESSION_SECRET="at least 32 characters"
Then:
pnpm db:migrate && pnpm db:seed
pnpm dev
Sign in at http://localhost:3000 with APP_PASSWORD. First run asks whether
the ledger is personal or a business — it picks the chart of accounts, and is
switchable later. Upload a CSV and it starts classifying.
Everything else is optional and the app is fully usable without it: no API key means the classifier runs on rules alone, and no Plaid credentials means you upload statements by hand.
| Optional | Set |
|---|---|
| Model-backed classification and PDF statements | AI_API_KEY, AI_PROVIDER, AI_MODEL |
| Bank syncing | PLAID_CLIENT_ID, PLAID_SECRET, PLAID_ENV, PLAID_TOKEN_KEY |
| Push alerts | VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY |
| SMS alerts | TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM, ALERT_PHONE |
| Scheduled sync | CRON_SECRET |
.env.example documents all of them.
Any Node host and any Postgres. Docker Compose and Vercel are both covered in docs/deploy.md.
A prebuilt image is published to GitHub Container Registry on every push to
main and every release tag:
docker pull ghcr.io/aroesec/loot:0.3.0
Note that docker-compose.yml builds from source rather than using it, so the
image is for your own compose file or orchestrator. Pin to 0.3.0 or a sha-
tag for anything you care about: latest moves, and registry tags carry no v
prefix even though the git tags do.
| deploy.md | Hosting, Docker, migrations |
| security.md | Authentication, rate limiting, proxies |
| classification.md | How transactions get categorized |
| backups.md | Taking and restoring dumps |
| notifications.md | Push, SMS, installing on iOS |
| plaid.md | Bank syncing |
| ai.md | Model providers, including local ones |
| extending.md | Adding sources, providers and channels |
| AGENTS.md | Conventions and invariants, for humans and coding agents |
| DESIGN.md | Why the design is the way it is |
| TESTING.md | What is worth testing here, and what is not |
| CHANGELOG.md | What changed in each release |
pnpm dev build test typecheck | the usual |
pnpm auth:hash '<password>' | generate APP_PASSWORD_HASH |
pnpm db:migrate then db:seed | schema, then the chart of accounts |
pnpm db:reclassify then db:reconcile-debt | re-file history; always in that order, or real spending disappears |
pnpm db:backup | verified pg_dump into backups/ |
pnpm db:audit-income db:audit-period db:audit-splits | prove the totals from outside the code |
package.json is the source of truth for the rest.
Next.js 15 with the App Router, Postgres via Drizzle, Tailwind. The Anthropic and Plaid SDKs sit behind interfaces and are optional at runtime, so neither is required to run the app.
CONTRIBUTING.md covers setup and the invariants worth knowing before changing anything. DESIGN.md explains why the design is the way it is, usually by naming the bug that caused it.
Security reports go through private advisories rather than public issues. See SECURITY.md.
MIT. See LICENSE.
40 commits
4 commits
TypeScript
98.8%