A zero-based budget app with accounts, categories, envelopes, and reports is very buildable; bank sync and habit coaching are the main paid value.
You are building a lean indie version of YNAB.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# YNAB · indie build
A zero-based budget you own: accounts and categories, a monthly grid where every dollar is assigned, a To Be Budgeted figure that turns red when you over-assign, CSV import with remembered column mappings and payee rules, reconciliation against the real bank balance, credit cards handled the way a budget should, and optional bank sync through SimpleFIN for about $15 a year instead of YNAB's price.
Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22 with Express and better-sqlite3 | many views and forms; the framework earns its place |
| Money | Integer cents, never floats | a budget that disagrees with the bank by a cent loses all trust |
| Dates | YYYY-MM-DD strings, not timestamps | a transaction happens on a date; timezone shifts move money between months |
| Hosting | localhost only | this is your money; it does not need to be on the internet |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Node.js 22 or newer** · free
- Why: Everything in this build runs on it: the server, the scripts, the tests.
- Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards.
- Verify: node --version prints v22 or higher
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
- [ ] **A CSV export from each bank account** · free
- Why: Phase 4 imports real data; you need a file per account to test column mapping and sign conventions.
- Get it: In your bank's web app, find Export or Download transactions, choose CSV, last 90 days. Save one per account, including a credit card.
- [ ] **Your category groups and categories, written down** · free
- Why: The budget grid is built on them. Deciding in a spreadsheet first stops Phase 2 from becoming a planning session.
- Get it: Groups like Bills, Everyday, Savings Goals; six to fifteen categories total to start.
- [ ] **Today's real balance for every account** · free
- Why: Reconciliation in Phase 5 needs the true figure.
- Get it: Read them off the bank apps now and note the date.
- [ ] **A SimpleFIN Bridge access token (optional)** (optional) · about $15 a year
- Why: Automatic bank sync, the one thing CSV import cannot replace. About $15 a year, and what Actual Budget uses for the same job. You never hold bank credentials; the aggregator does.
- Get it: beta-bridge.simplefin.org > sign up > connect your banks > create an access token (a setup token you exchange once for an access URL). Put the access URL in .env.
## Quick start
```sh
mkdir budget && cd budget && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir data && cp .env.example .env
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Mobile apps and family sharing beyond two logins.
- The educational method and habit design, which is a real part of what YNAB sells.
- Polished reports.
- bank sync
- mobile apps
- educational method/content
- family sharing
- polished reports
- support
- habit design
If one of those is essential to you, that is the reason to keep paying for YNAB, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · YNAB
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a zero-based budgeting app to replace YNAB. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost only.
- Money is stored as integer minor units (cents). Never a float. A budgeting app
that uses floating point will disagree with the bank by a cent and destroy the
user's trust in every number on the screen.
- Dates are plain `YYYY-MM-DD` strings, not timestamps. A transaction happens on
a date, not at an instant, and timezone-shifting one across a month boundary
moves money between budgets.
### Data model (create this before Phase 1)
- `accounts`: id, name, kind ('checking' | 'savings' | 'cash' | 'credit'),
on_budget (bool), closed (bool)
- `category_groups`: id, name, position
- `categories`: id, group_id, name, position, hidden
- `budgets`: id, month ('YYYY-MM'), category_id, assigned_cents
- `transactions`: id, account_id, date, payee, category_id (nullable),
amount_cents (negative is outflow), memo, cleared (bool), reconciled (bool),
transfer_transaction_id (nullable)
- `payee_rules`: payee_pattern, category_id
Every amount is signed from the account's perspective. A transfer is two rows
pointing at each other, not one row with two accounts · anything else makes
reconciliation impossible to reason about later.
### Phase 1 · Accounts and transactions
Build: account CRUD, and a fast keyboard-first transaction entry form (date,
payee, category, amount, memo). Running account balances computed by query, not
stored on the account row · a cached balance will drift and there is no way to
tell which number is the lie.
Done when: entering ten transactions across two accounts yields balances matching
a hand sum, editing an amount updates the balance, deleting restores it, and
entering `12.10` stores `1210` and renders `$12.10` exactly.
Do not build yet: categories, the budget, import.
### Phase 2 · Categories and the budget grid
Build: category groups and categories, then the monthly budget view · a grid of
category, assigned, activity and available. `activity` is the sum of that
category's transactions in that month. `available` is the previous month's
available plus this month's assigned plus activity, so unspent money rolls
forward.
Done when: assigning $200 to Groceries and spending $50 shows assigned 200,
activity -50, available 150; the next month opens with 150 available before
anything is assigned; and a category overspent to -20 carries the negative into
the next month rather than silently resetting.
### Phase 3 · To Be Budgeted
Build: the header figure · total inflow to on-budget accounts minus everything
assigned across all months. Show it prominently, and turn it red when negative.
This number is the entire method: every dollar has a job, and the app's job is to
tell you when one does not.
Done when: adding $1,000 of income raises it by exactly 1000, assigning $400
lowers it by 400, over-assigning turns it red with the correct negative figure,
and it reconciles with a hand-written SQL query on a seeded fixture.
### Phase 4 · CSV import
Build: import from a bank CSV. Present a column-mapping step on first import per
account (date, payee, amount, or separate debit/credit columns) and remember the
mapping. Parse dates without guessing between `DD/MM` and `MM/DD` · ask once and
store the answer, because guessing wrong silently shifts transactions by months.
Deduplicate against existing rows on date, amount and payee, and show what will
be skipped before committing. Apply `payee_rules` so repeat payees auto-fill
their category, and learn a rule whenever the user categorizes a new payee.
Done when: importing the same file twice adds nothing the second time, an
ambiguous date format is asked about rather than assumed, a credit-card CSV with
inverted signs imports with correct polarity, and a known payee arrives
pre-categorized.
### Phase 5 · Reconciliation
Build: the reconcile flow · enter the real bank balance, the app shows the
difference against the cleared balance, the user ticks transactions cleared until
it reaches zero, then locks them as reconciled. Offer to create a balance
adjustment transaction for a remaining difference rather than editing history.
Done when: a $3 discrepancy is reported precisely, ticking the missing
transaction clears it, reconciled transactions resist accidental edits, and the
adjustment path leaves an auditable row rather than a silent change.
### Phase 6 · Credit cards
Build: credit-card handling · spending in a category on a credit card moves the
assigned money to that card's payment category, so the budget shows money set
aside to pay the bill. This is the part every simple budgeting app gets wrong,
and it is the difference between a spending tracker and a budget.
Done when: a $50 grocery purchase on a credit card reduces Groceries available by
50 and increases the card's payment category by 50, and paying the card as a
transfer reduces both correctly.
### Phase 7 · Reports and backups
Build: spending by category per month and net worth over time as inline SVG or
Chart.js, plus a nightly copy of the database to `backups/budget-YYYY-MM-DD.db`
keeping 30, and a documented restore.
Done when: the report totals match the transaction table, and a restore has been
performed once for real.
### Phase 8 · Optional bank sync, honestly priced
Build: this phase is optional and you should read the numbers before starting it.
Bank aggregation is the one thing CSV import cannot replace, and it does not have
to cost what YNAB costs · SimpleFIN Bridge is roughly $15/year and is what Actual
Budget uses for the same job. Put it behind an interface with the CSV importer as
the other implementation, keep the token in `.env`, and never store bank
credentials yourself · the aggregator holds them, which is the entire reason to
use one.
Done when: a sync pulls new transactions, dedupes against manual entries, and the
app still works completely with the integration disabled and no token present.
### Out of scope (and why)
- Mobile apps and family sharing.
- The educational method and habit design, which is a real part of what YNAB
sells · the software is the smaller half of that product.
- Polished reports.
### README must contain
- The CSV column-mapping step and how to redo a mapping.
- The credit-card model explained in three sentences, because it will look wrong
to anyone expecting a spending tracker.
- The bank-sync cost, with the date checked, and a statement that it is optional.
===== AGENTS.md =====
# Agent instructions · YNAB indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with Express and better-sqlite3, Integer cents, never floats, YYYY-MM-DD strings, not timestamps, localhost only. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
===== BUILD_PLAN.md =====
# Build plan · YNAB
A zero-based budget you own: accounts and categories, a monthly grid where every dollar is assigned, a To Be Budgeted figure that turns red when you over-assign, CSV import with remembered column mappings and payee rules, reconciliation against the real bank balance, credit cards handled the way a budget should, and optional bank sync through SimpleFIN for about $15 a year instead of YNAB's price.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Accounts and transactions
Fast keyboard entry, balances computed by query, cents stored exactly.
### Steps
1. Create the project and the tables
accounts (id, name, kind, on_budget, closed), category_groups, categories, budgets (month, category_id, assigned_cents), transactions (id, account_id, date, payee, category_id, amount_cents signed, memo, cleared, reconciled, transfer_transaction_id), payee_rules.
```sh
mkdir budget && cd budget && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir data && cp .env.example .env
```
2. Build account CRUD and the transaction entry form
Keyboard-first: date, payee, category, amount, memo, enter to save. Parse 12.10 into 1210; never store a float.
3. Compute balances by query, not stored
A cached balance drifts and you cannot tell which number lies.
### Done when
- [ ] Ten transactions across two accounts yield balances matching a hand sum
- [ ] Editing an amount updates the balance; deleting restores it
- [ ] Entering 12.10 stores 1210 and renders $12.10 exactly
## Phase 2 · Categories and the budget grid
Assigned, activity and available per category per month, with rollover.
### Steps
1. Build groups and categories from your list
2. Build the monthly grid
activity = sum of the category's transactions that month; available = last month's available + assigned + activity.
### Done when
- [ ] Assign $200 to Groceries, spend $50: assigned 200, activity -50, available 150
- [ ] Next month opens with 150 available before anything is assigned
- [ ] An overspent -20 carries into the next month rather than resetting
## Phase 3 · To Be Budgeted
The header number the whole method rests on.
### Steps
1. Compute inflow to on-budget accounts minus everything assigned across all months
One query; reconcile it against a hand calculation on a fixture.
2. Show it in the header on every budget page, red when negative
This number is the entire method: every dollar has a job, and the app's job is to tell you when one does not.
### Done when
- [ ] Adding $1,000 income raises it by exactly 1000
- [ ] Assigning $400 lowers it by 400
- [ ] Over-assigning turns it red with the correct negative figure
## Phase 4 · CSV import
Import any bank's CSV once you have mapped it, dedupe, and learn payees.
### Steps
1. Build the column-mapping step per account
Date, payee, amount or debit/credit columns. Ask the date format once and store it; never guess between DD/MM and MM/DD.
2. Dedupe on date, amount and payee and preview what will be skipped
3. Apply payee_rules and learn a rule when you categorize a new payee
### Done when
- [ ] Importing the same file twice adds nothing the second time
- [ ] An ambiguous date format is asked about rather than assumed
- [ ] A credit-card CSV with inverted signs imports with correct polarity
- [ ] A known payee arrives pre-categorized
## Phase 5 · Reconciliation
Match the bank to the cent and lock what matched.
### Steps
1. Build the reconcile flow
Enter the real balance, see the difference against cleared, tick transactions cleared until zero, lock them reconciled.
2. Offer a balance adjustment transaction for a remaining difference
An auditable row rather than a silent edit of history.
### Done when
- [ ] A $3 discrepancy is reported precisely
- [ ] Ticking the missing transaction clears it
- [ ] Reconciled transactions resist accidental edits
- [ ] The adjustment path leaves an auditable row
## Phase 6 · Credit cards
Spending on a card moves the assigned money to the card's payment category.
### Steps
1. Implement the credit-card payment category rule
A categorized purchase on a card reduces that category's available and raises the card's payment category by the same amount.
2. Treat paying the card as a transfer that reduces both
Show the payment category in the grid so the money set aside for the bill is visible.
### Done when
- [ ] A $50 grocery purchase on a card reduces Groceries by 50 and raises the card payment category by 50
- [ ] Paying the card as a transfer reduces both correctly
## Phase 7 · Reports and backups
Spending by category and net worth, and a backup you have restored.
### Steps
1. Build the two reports as inline SVG
2. Nightly copy of the database to backups/, thirty kept, and one restore performed
```sh
sqlite3 data/budget.db ".backup 'backups/budget-$(date +%F).db'"
```
### Done when
- [ ] Report totals match the transaction table
- [ ] A restore has been performed once
## Phase 8 · Optional bank sync, honestly priced
SimpleFIN behind an interface, with CSV as the other implementation and no bank credentials in your app.
### Steps
1. Exchange the setup token for the access URL once
```sh
curl -s $(echo $SETUP_TOKEN | base64 -d) # the decoded setup token is a claim URL; POST to it once to receive the access URL
```
2. Implement the sync source
GET the access URL's /accounts, map transactions to your model, dedupe against manual entries by date, amount and payee.
### Done when
- [ ] A sync pulls new transactions and dedupes against manual entries
- [ ] The app works completely with sync disabled and no token present
## Not in this build
- Mobile apps and family sharing beyond two logins.
- The educational method and habit design, which is a real part of what YNAB sells.
- Polished reports.
## After v1, if you want it
- Goals per category (save X by month Y) shown in the grid
- A PWA shell so the phone can add a transaction
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Bound to 127.0.0.1 only.
PORT=4820
# Required. Your money. Back it up.
DATABASE_PATH=./data/budget.db
# Required. ISO code for formatting.
CURRENCY=USD
# Required. Your bank's CSV date format, asked once per account on import and stored.
DATE_FORMAT_HINT=MM/DD/YYYY
# Optional · secret. From SimpleFIN after exchanging the setup token. Empty disables sync.
SIMPLEFIN_ACCESS_URL=https://...:...@beta-bridge.simplefin.org/simplefin
You are building a lean indie version of YNAB.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# YNAB · indie build
A zero-based budget you own: accounts and categories, a monthly grid where every dollar is assigned, a To Be Budgeted figure that turns red when you over-assign, CSV import with remembered column mappings and payee rules, reconciliation against the real bank balance, credit cards handled the way a budget should, and optional bank sync through SimpleFIN for about $15 a year instead of YNAB's price.
Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22 with Express and better-sqlite3 | many views and forms; the framework earns its place |
| Money | Integer cents, never floats | a budget that disagrees with the bank by a cent loses all trust |
| Dates | YYYY-MM-DD strings, not timestamps | a transaction happens on a date; timezone shifts move money between months |
| Hosting | localhost only | this is your money; it does not need to be on the internet |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Node.js 22 or newer** · free
- Why: Everything in this build runs on it: the server, the scripts, the tests.
- Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards.
- Verify: node --version prints v22 or higher
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
- [ ] **A CSV export from each bank account** · free
- Why: Phase 4 imports real data; you need a file per account to test column mapping and sign conventions.
- Get it: In your bank's web app, find Export or Download transactions, choose CSV, last 90 days. Save one per account, including a credit card.
- [ ] **Your category groups and categories, written down** · free
- Why: The budget grid is built on them. Deciding in a spreadsheet first stops Phase 2 from becoming a planning session.
- Get it: Groups like Bills, Everyday, Savings Goals; six to fifteen categories total to start.
- [ ] **Today's real balance for every account** · free
- Why: Reconciliation in Phase 5 needs the true figure.
- Get it: Read them off the bank apps now and note the date.
- [ ] **A SimpleFIN Bridge access token (optional)** (optional) · about $15 a year
- Why: Automatic bank sync, the one thing CSV import cannot replace. About $15 a year, and what Actual Budget uses for the same job. You never hold bank credentials; the aggregator does.
- Get it: beta-bridge.simplefin.org > sign up > connect your banks > create an access token (a setup token you exchange once for an access URL). Put the access URL in .env.
## Quick start
```sh
mkdir budget && cd budget && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir data && cp .env.example .env
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Mobile apps and family sharing beyond two logins.
- The educational method and habit design, which is a real part of what YNAB sells.
- Polished reports.
- bank sync
- mobile apps
- educational method/content
- family sharing
- polished reports
- support
- habit design
If one of those is essential to you, that is the reason to keep paying for YNAB, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · YNAB
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a zero-based budgeting app to replace YNAB. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost only.
- Money is stored as integer minor units (cents). Never a float. A budgeting app
that uses floating point will disagree with the bank by a cent and destroy the
user's trust in every number on the screen.
- Dates are plain `YYYY-MM-DD` strings, not timestamps. A transaction happens on
a date, not at an instant, and timezone-shifting one across a month boundary
moves money between budgets.
### Data model (create this before Phase 1)
- `accounts`: id, name, kind ('checking' | 'savings' | 'cash' | 'credit'),
on_budget (bool), closed (bool)
- `category_groups`: id, name, position
- `categories`: id, group_id, name, position, hidden
- `budgets`: id, month ('YYYY-MM'), category_id, assigned_cents
- `transactions`: id, account_id, date, payee, category_id (nullable),
amount_cents (negative is outflow), memo, cleared (bool), reconciled (bool),
transfer_transaction_id (nullable)
- `payee_rules`: payee_pattern, category_id
Every amount is signed from the account's perspective. A transfer is two rows
pointing at each other, not one row with two accounts · anything else makes
reconciliation impossible to reason about later.
### Phase 1 · Accounts and transactions
Build: account CRUD, and a fast keyboard-first transaction entry form (date,
payee, category, amount, memo). Running account balances computed by query, not
stored on the account row · a cached balance will drift and there is no way to
tell which number is the lie.
Done when: entering ten transactions across two accounts yields balances matching
a hand sum, editing an amount updates the balance, deleting restores it, and
entering `12.10` stores `1210` and renders `$12.10` exactly.
Do not build yet: categories, the budget, import.
### Phase 2 · Categories and the budget grid
Build: category groups and categories, then the monthly budget view · a grid of
category, assigned, activity and available. `activity` is the sum of that
category's transactions in that month. `available` is the previous month's
available plus this month's assigned plus activity, so unspent money rolls
forward.
Done when: assigning $200 to Groceries and spending $50 shows assigned 200,
activity -50, available 150; the next month opens with 150 available before
anything is assigned; and a category overspent to -20 carries the negative into
the next month rather than silently resetting.
### Phase 3 · To Be Budgeted
Build: the header figure · total inflow to on-budget accounts minus everything
assigned across all months. Show it prominently, and turn it red when negative.
This number is the entire method: every dollar has a job, and the app's job is to
tell you when one does not.
Done when: adding $1,000 of income raises it by exactly 1000, assigning $400
lowers it by 400, over-assigning turns it red with the correct negative figure,
and it reconciles with a hand-written SQL query on a seeded fixture.
### Phase 4 · CSV import
Build: import from a bank CSV. Present a column-mapping step on first import per
account (date, payee, amount, or separate debit/credit columns) and remember the
mapping. Parse dates without guessing between `DD/MM` and `MM/DD` · ask once and
store the answer, because guessing wrong silently shifts transactions by months.
Deduplicate against existing rows on date, amount and payee, and show what will
be skipped before committing. Apply `payee_rules` so repeat payees auto-fill
their category, and learn a rule whenever the user categorizes a new payee.
Done when: importing the same file twice adds nothing the second time, an
ambiguous date format is asked about rather than assumed, a credit-card CSV with
inverted signs imports with correct polarity, and a known payee arrives
pre-categorized.
### Phase 5 · Reconciliation
Build: the reconcile flow · enter the real bank balance, the app shows the
difference against the cleared balance, the user ticks transactions cleared until
it reaches zero, then locks them as reconciled. Offer to create a balance
adjustment transaction for a remaining difference rather than editing history.
Done when: a $3 discrepancy is reported precisely, ticking the missing
transaction clears it, reconciled transactions resist accidental edits, and the
adjustment path leaves an auditable row rather than a silent change.
### Phase 6 · Credit cards
Build: credit-card handling · spending in a category on a credit card moves the
assigned money to that card's payment category, so the budget shows money set
aside to pay the bill. This is the part every simple budgeting app gets wrong,
and it is the difference between a spending tracker and a budget.
Done when: a $50 grocery purchase on a credit card reduces Groceries available by
50 and increases the card's payment category by 50, and paying the card as a
transfer reduces both correctly.
### Phase 7 · Reports and backups
Build: spending by category per month and net worth over time as inline SVG or
Chart.js, plus a nightly copy of the database to `backups/budget-YYYY-MM-DD.db`
keeping 30, and a documented restore.
Done when: the report totals match the transaction table, and a restore has been
performed once for real.
### Phase 8 · Optional bank sync, honestly priced
Build: this phase is optional and you should read the numbers before starting it.
Bank aggregation is the one thing CSV import cannot replace, and it does not have
to cost what YNAB costs · SimpleFIN Bridge is roughly $15/year and is what Actual
Budget uses for the same job. Put it behind an interface with the CSV importer as
the other implementation, keep the token in `.env`, and never store bank
credentials yourself · the aggregator holds them, which is the entire reason to
use one.
Done when: a sync pulls new transactions, dedupes against manual entries, and the
app still works completely with the integration disabled and no token present.
### Out of scope (and why)
- Mobile apps and family sharing.
- The educational method and habit design, which is a real part of what YNAB
sells · the software is the smaller half of that product.
- Polished reports.
### README must contain
- The CSV column-mapping step and how to redo a mapping.
- The credit-card model explained in three sentences, because it will look wrong
to anyone expecting a spending tracker.
- The bank-sync cost, with the date checked, and a statement that it is optional.
===== AGENTS.md =====
# Agent instructions · YNAB indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with Express and better-sqlite3, Integer cents, never floats, YYYY-MM-DD strings, not timestamps, localhost only. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
===== BUILD_PLAN.md =====
# Build plan · YNAB
A zero-based budget you own: accounts and categories, a monthly grid where every dollar is assigned, a To Be Budgeted figure that turns red when you over-assign, CSV import with remembered column mappings and payee rules, reconciliation against the real bank balance, credit cards handled the way a budget should, and optional bank sync through SimpleFIN for about $15 a year instead of YNAB's price.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Accounts and transactions
Fast keyboard entry, balances computed by query, cents stored exactly.
### Steps
1. Create the project and the tables
accounts (id, name, kind, on_budget, closed), category_groups, categories, budgets (month, category_id, assigned_cents), transactions (id, account_id, date, payee, category_id, amount_cents signed, memo, cleared, reconciled, transfer_transaction_id), payee_rules.
```sh
mkdir budget && cd budget && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir data && cp .env.example .env
```
2. Build account CRUD and the transaction entry form
Keyboard-first: date, payee, category, amount, memo, enter to save. Parse 12.10 into 1210; never store a float.
3. Compute balances by query, not stored
A cached balance drifts and you cannot tell which number lies.
### Done when
- [ ] Ten transactions across two accounts yield balances matching a hand sum
- [ ] Editing an amount updates the balance; deleting restores it
- [ ] Entering 12.10 stores 1210 and renders $12.10 exactly
## Phase 2 · Categories and the budget grid
Assigned, activity and available per category per month, with rollover.
### Steps
1. Build groups and categories from your list
2. Build the monthly grid
activity = sum of the category's transactions that month; available = last month's available + assigned + activity.
### Done when
- [ ] Assign $200 to Groceries, spend $50: assigned 200, activity -50, available 150
- [ ] Next month opens with 150 available before anything is assigned
- [ ] An overspent -20 carries into the next month rather than resetting
## Phase 3 · To Be Budgeted
The header number the whole method rests on.
### Steps
1. Compute inflow to on-budget accounts minus everything assigned across all months
One query; reconcile it against a hand calculation on a fixture.
2. Show it in the header on every budget page, red when negative
This number is the entire method: every dollar has a job, and the app's job is to tell you when one does not.
### Done when
- [ ] Adding $1,000 income raises it by exactly 1000
- [ ] Assigning $400 lowers it by 400
- [ ] Over-assigning turns it red with the correct negative figure
## Phase 4 · CSV import
Import any bank's CSV once you have mapped it, dedupe, and learn payees.
### Steps
1. Build the column-mapping step per account
Date, payee, amount or debit/credit columns. Ask the date format once and store it; never guess between DD/MM and MM/DD.
2. Dedupe on date, amount and payee and preview what will be skipped
3. Apply payee_rules and learn a rule when you categorize a new payee
### Done when
- [ ] Importing the same file twice adds nothing the second time
- [ ] An ambiguous date format is asked about rather than assumed
- [ ] A credit-card CSV with inverted signs imports with correct polarity
- [ ] A known payee arrives pre-categorized
## Phase 5 · Reconciliation
Match the bank to the cent and lock what matched.
### Steps
1. Build the reconcile flow
Enter the real balance, see the difference against cleared, tick transactions cleared until zero, lock them reconciled.
2. Offer a balance adjustment transaction for a remaining difference
An auditable row rather than a silent edit of history.
### Done when
- [ ] A $3 discrepancy is reported precisely
- [ ] Ticking the missing transaction clears it
- [ ] Reconciled transactions resist accidental edits
- [ ] The adjustment path leaves an auditable row
## Phase 6 · Credit cards
Spending on a card moves the assigned money to the card's payment category.
### Steps
1. Implement the credit-card payment category rule
A categorized purchase on a card reduces that category's available and raises the card's payment category by the same amount.
2. Treat paying the card as a transfer that reduces both
Show the payment category in the grid so the money set aside for the bill is visible.
### Done when
- [ ] A $50 grocery purchase on a card reduces Groceries by 50 and raises the card payment category by 50
- [ ] Paying the card as a transfer reduces both correctly
## Phase 7 · Reports and backups
Spending by category and net worth, and a backup you have restored.
### Steps
1. Build the two reports as inline SVG
2. Nightly copy of the database to backups/, thirty kept, and one restore performed
```sh
sqlite3 data/budget.db ".backup 'backups/budget-$(date +%F).db'"
```
### Done when
- [ ] Report totals match the transaction table
- [ ] A restore has been performed once
## Phase 8 · Optional bank sync, honestly priced
SimpleFIN behind an interface, with CSV as the other implementation and no bank credentials in your app.
### Steps
1. Exchange the setup token for the access URL once
```sh
curl -s $(echo $SETUP_TOKEN | base64 -d) # the decoded setup token is a claim URL; POST to it once to receive the access URL
```
2. Implement the sync source
GET the access URL's /accounts, map transactions to your model, dedupe against manual entries by date, amount and payee.
### Done when
- [ ] A sync pulls new transactions and dedupes against manual entries
- [ ] The app works completely with sync disabled and no token present
## Not in this build
- Mobile apps and family sharing beyond two logins.
- The educational method and habit design, which is a real part of what YNAB sells.
- Polished reports.
## After v1, if you want it
- Goals per category (save X by month Y) shown in the grid
- A PWA shell so the phone can add a transaction
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Bound to 127.0.0.1 only.
PORT=4820
# Required. Your money. Back it up.
DATABASE_PATH=./data/budget.db
# Required. ISO code for formatting.
CURRENCY=USD
# Required. Your bank's CSV date format, asked once per account on import and stored.
DATE_FORMAT_HINT=MM/DD/YYYY
# Optional · secret. From SimpleFIN after exchanging the setup token. Empty disables sync.
SIMPLEFIN_ACCESS_URL=https://...:...@beta-bridge.simplefin.org/simplefin
You are building a production product version of YNAB.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== PRODUCT.md =====
# YNAB · product brief
## Problem
A zero-based budget app with accounts, categories, envelopes, and reports is very buildable; bank sync and habit coaching are the main paid value.
## Product outcome
A budget two people can share with confidence: exact money maths, imports that never double, a bank sync you chose knowingly, backups encrypted off the box.
## Target user
A builder who needs a maintainable product foundation, not a one-off demo.
## Required capabilities
- local or hosted database
- CSV import
- optional bank aggregation API
- reporting charts
- backups
## Explicit non-goals for v1
- Mobile apps and family sharing beyond two logins.
- The educational method and habit design, which is a real part of what YNAB sells.
- Polished reports.
- bank sync
- mobile apps
- educational method/content
- family sharing
- polished reports
- support
- habit design
## Success criteria
- Money maths verified against a hand-built month
- Import dedupe verified on a real bank file twice
- One restore drill performed
- Sync disabled path verified
===== BRIEF.md =====
# Build brief · YNAB
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a zero-based budgeting app to replace YNAB. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost only.
- Money is stored as integer minor units (cents). Never a float. A budgeting app
that uses floating point will disagree with the bank by a cent and destroy the
user's trust in every number on the screen.
- Dates are plain `YYYY-MM-DD` strings, not timestamps. A transaction happens on
a date, not at an instant, and timezone-shifting one across a month boundary
moves money between budgets.
### Data model (create this before Phase 1)
- `accounts`: id, name, kind ('checking' | 'savings' | 'cash' | 'credit'),
on_budget (bool), closed (bool)
- `category_groups`: id, name, position
- `categories`: id, group_id, name, position, hidden
- `budgets`: id, month ('YYYY-MM'), category_id, assigned_cents
- `transactions`: id, account_id, date, payee, category_id (nullable),
amount_cents (negative is outflow), memo, cleared (bool), reconciled (bool),
transfer_transaction_id (nullable)
- `payee_rules`: payee_pattern, category_id
Every amount is signed from the account's perspective. A transfer is two rows
pointing at each other, not one row with two accounts · anything else makes
reconciliation impossible to reason about later.
### Phase 1 · Accounts and transactions
Build: account CRUD, and a fast keyboard-first transaction entry form (date,
payee, category, amount, memo). Running account balances computed by query, not
stored on the account row · a cached balance will drift and there is no way to
tell which number is the lie.
Done when: entering ten transactions across two accounts yields balances matching
a hand sum, editing an amount updates the balance, deleting restores it, and
entering `12.10` stores `1210` and renders `$12.10` exactly.
Do not build yet: categories, the budget, import.
### Phase 2 · Categories and the budget grid
Build: category groups and categories, then the monthly budget view · a grid of
category, assigned, activity and available. `activity` is the sum of that
category's transactions in that month. `available` is the previous month's
available plus this month's assigned plus activity, so unspent money rolls
forward.
Done when: assigning $200 to Groceries and spending $50 shows assigned 200,
activity -50, available 150; the next month opens with 150 available before
anything is assigned; and a category overspent to -20 carries the negative into
the next month rather than silently resetting.
### Phase 3 · To Be Budgeted
Build: the header figure · total inflow to on-budget accounts minus everything
assigned across all months. Show it prominently, and turn it red when negative.
This number is the entire method: every dollar has a job, and the app's job is to
tell you when one does not.
Done when: adding $1,000 of income raises it by exactly 1000, assigning $400
lowers it by 400, over-assigning turns it red with the correct negative figure,
and it reconciles with a hand-written SQL query on a seeded fixture.
### Phase 4 · CSV import
Build: import from a bank CSV. Present a column-mapping step on first import per
account (date, payee, amount, or separate debit/credit columns) and remember the
mapping. Parse dates without guessing between `DD/MM` and `MM/DD` · ask once and
store the answer, because guessing wrong silently shifts transactions by months.
Deduplicate against existing rows on date, amount and payee, and show what will
be skipped before committing. Apply `payee_rules` so repeat payees auto-fill
their category, and learn a rule whenever the user categorizes a new payee.
Done when: importing the same file twice adds nothing the second time, an
ambiguous date format is asked about rather than assumed, a credit-card CSV with
inverted signs imports with correct polarity, and a known payee arrives
pre-categorized.
### Phase 5 · Reconciliation
Build: the reconcile flow · enter the real bank balance, the app shows the
difference against the cleared balance, the user ticks transactions cleared until
it reaches zero, then locks them as reconciled. Offer to create a balance
adjustment transaction for a remaining difference rather than editing history.
Done when: a $3 discrepancy is reported precisely, ticking the missing
transaction clears it, reconciled transactions resist accidental edits, and the
adjustment path leaves an auditable row rather than a silent change.
### Phase 6 · Credit cards
Build: credit-card handling · spending in a category on a credit card moves the
assigned money to that card's payment category, so the budget shows money set
aside to pay the bill. This is the part every simple budgeting app gets wrong,
and it is the difference between a spending tracker and a budget.
Done when: a $50 grocery purchase on a credit card reduces Groceries available by
50 and increases the card's payment category by 50, and paying the card as a
transfer reduces both correctly.
### Phase 7 · Reports and backups
Build: spending by category per month and net worth over time as inline SVG or
Chart.js, plus a nightly copy of the database to `backups/budget-YYYY-MM-DD.db`
keeping 30, and a documented restore.
Done when: the report totals match the transaction table, and a restore has been
performed once for real.
### Phase 8 · Optional bank sync, honestly priced
Build: this phase is optional and you should read the numbers before starting it.
Bank aggregation is the one thing CSV import cannot replace, and it does not have
to cost what YNAB costs · SimpleFIN Bridge is roughly $15/year and is what Actual
Budget uses for the same job. Put it behind an interface with the CSV importer as
the other implementation, keep the token in `.env`, and never store bank
credentials yourself · the aggregator holds them, which is the entire reason to
use one.
Done when: a sync pulls new transactions, dedupes against manual entries, and the
app still works completely with the integration disabled and no token present.
### Out of scope (and why)
- Mobile apps and family sharing.
- The educational method and habit design, which is a real part of what YNAB
sells · the software is the smaller half of that product.
- Polished reports.
### README must contain
- The CSV column-mapping step and how to redo a mapping.
- The credit-card model explained in three sentences, because it will look wrong
to anyone expecting a spending tracker.
- The bank-sync cost, with the date checked, and a statement that it is optional.
===== ARCHITECTURE.md =====
# Architecture · YNAB
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22 with Express and better-sqlite3 | many views and forms; the framework earns its place |
| Money | Integer cents, never floats | a budget that disagrees with the bank by a cent loses all trust |
| Dates | YYYY-MM-DD strings, not timestamps | a transaction happens on a date; timezone shifts move money between months |
| Hosting | localhost only | this is your money; it does not need to be on the internet |
## Modules
Each module has one owner concern and a documented way to replace it.
| Module | Owns | How to replace it |
| --- | --- | --- |
| Ledger | accounts, transactions, transfers, cents | The core; everything else reads it |
| Budget | assigned, activity, available, TBB | Pure queries over the ledger |
| Import | CSV mapping, dedupe, payee rules, and the SimpleFIN source | Add another aggregator as a second source |
| Reconcile | cleared and reconciled state | Rules only; no external dependency |
## Configuration
Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists.
- `PORT` · required · Bound to 127.0.0.1 only.
- `DATABASE_PATH` · required · Your money. Back it up.
- `CURRENCY` · required · ISO code for formatting.
- `DATE_FORMAT_HINT` · required · Your bank's CSV date format, asked once per account on import and stored.
- `SIMPLEFIN_ACCESS_URL` · optional, secret · From SimpleFIN after exchanging the setup token. Empty disables sync.
## Production baseline
- Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives.
- Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised.
- Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts.
- Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists.
- Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path.
## Decision records
For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
===== AGENTS.md =====
# Agent instructions · YNAB product build
- Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with Express and better-sqlite3, Integer cents, never floats, YYYY-MM-DD strings, not timestamps, localhost only.
- Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit.
- Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present.
- Never invent cryptography or silently weaken a requirement to make a check pass.
- Put every external service behind an interface with a deterministic fake for tests.
- Add migrations and rollback or recovery notes for every persistent data change.
- Log useful operational context without credentials, tokens, passwords or personal data.
- Update documentation and run every check before completing a milestone.
===== MILESTONES.md =====
# Delivery milestones · YNAB
Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer.
## M1 · Accounts and transactions
Fast keyboard entry, balances computed by query, cents stored exactly.
### Steps
1. Create the project and the tables
accounts (id, name, kind, on_budget, closed), category_groups, categories, budgets (month, category_id, assigned_cents), transactions (id, account_id, date, payee, category_id, amount_cents signed, memo, cleared, reconciled, transfer_transaction_id), payee_rules.
```sh
mkdir budget && cd budget && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir data && cp .env.example .env
```
2. Build account CRUD and the transaction entry form
Keyboard-first: date, payee, category, amount, memo, enter to save. Parse 12.10 into 1210; never store a float.
3. Compute balances by query, not stored
A cached balance drifts and you cannot tell which number lies.
### Done when
- [ ] Ten transactions across two accounts yield balances matching a hand sum
- [ ] Editing an amount updates the balance; deleting restores it
- [ ] Entering 12.10 stores 1210 and renders $12.10 exactly
## M2 · Categories and the budget grid
Assigned, activity and available per category per month, with rollover.
### Steps
1. Build groups and categories from your list
2. Build the monthly grid
activity = sum of the category's transactions that month; available = last month's available + assigned + activity.
### Done when
- [ ] Assign $200 to Groceries, spend $50: assigned 200, activity -50, available 150
- [ ] Next month opens with 150 available before anything is assigned
- [ ] An overspent -20 carries into the next month rather than resetting
## M3 · To Be Budgeted
The header number the whole method rests on.
### Steps
1. Compute inflow to on-budget accounts minus everything assigned across all months
One query; reconcile it against a hand calculation on a fixture.
2. Show it in the header on every budget page, red when negative
This number is the entire method: every dollar has a job, and the app's job is to tell you when one does not.
### Done when
- [ ] Adding $1,000 income raises it by exactly 1000
- [ ] Assigning $400 lowers it by 400
- [ ] Over-assigning turns it red with the correct negative figure
## M4 · CSV import
Import any bank's CSV once you have mapped it, dedupe, and learn payees.
### Steps
1. Build the column-mapping step per account
Date, payee, amount or debit/credit columns. Ask the date format once and store it; never guess between DD/MM and MM/DD.
2. Dedupe on date, amount and payee and preview what will be skipped
3. Apply payee_rules and learn a rule when you categorize a new payee
### Done when
- [ ] Importing the same file twice adds nothing the second time
- [ ] An ambiguous date format is asked about rather than assumed
- [ ] A credit-card CSV with inverted signs imports with correct polarity
- [ ] A known payee arrives pre-categorized
## M5 · Reconciliation
Match the bank to the cent and lock what matched.
### Steps
1. Build the reconcile flow
Enter the real balance, see the difference against cleared, tick transactions cleared until zero, lock them reconciled.
2. Offer a balance adjustment transaction for a remaining difference
An auditable row rather than a silent edit of history.
### Done when
- [ ] A $3 discrepancy is reported precisely
- [ ] Ticking the missing transaction clears it
- [ ] Reconciled transactions resist accidental edits
- [ ] The adjustment path leaves an auditable row
## M6 · Credit cards
Spending on a card moves the assigned money to the card's payment category.
### Steps
1. Implement the credit-card payment category rule
A categorized purchase on a card reduces that category's available and raises the card's payment category by the same amount.
2. Treat paying the card as a transfer that reduces both
Show the payment category in the grid so the money set aside for the bill is visible.
### Done when
- [ ] A $50 grocery purchase on a card reduces Groceries by 50 and raises the card payment category by 50
- [ ] Paying the card as a transfer reduces both correctly
## M7 · Reports and backups
Spending by category and net worth, and a backup you have restored.
### Steps
1. Build the two reports as inline SVG
2. Nightly copy of the database to backups/, thirty kept, and one restore performed
```sh
sqlite3 data/budget.db ".backup 'backups/budget-$(date +%F).db'"
```
### Done when
- [ ] Report totals match the transaction table
- [ ] A restore has been performed once
## M8 · Optional bank sync, honestly priced
SimpleFIN behind an interface, with CSV as the other implementation and no bank credentials in your app.
### Steps
1. Exchange the setup token for the access URL once
```sh
curl -s $(echo $SETUP_TOKEN | base64 -d) # the decoded setup token is a claim URL; POST to it once to receive the access URL
```
2. Implement the sync source
GET the access URL's /accounts, map transactions to your model, dedupe against manual entries by date, amount and payee.
### Done when
- [ ] A sync pulls new transactions and dedupes against manual entries
- [ ] The app works completely with sync disabled and no token present
## M9 · Run it on a server for two people (production only)
Only if you share the budget: HTTPS, a login, off-box backups.
### Steps
1. Add a single-user login and bind behind Caddy with HTTPS
2. Nightly off-box encrypted backup with age
```sh
age -r $AGE_RECIPIENT -o backups/budget-$(date +%F).db.age data/budget.db
```
3. Uptime check and structured logs
### Done when
- [ ] The login is required for every route
- [ ] A restore from the encrypted backup opens correctly
===== OPERATIONS.md =====
# Operations · YNAB
## Backup
Nightly SQLite backup, encrypted with age when off the box.
## Restore
Decrypt, copy back, open; check TBB matches the last known figure.
Do a restore drill before the first real user, and write the date here when it passes.
## Monitoring
Uptime on /healthz when hosted; a failed sync is shown in the UI.
## Incident checklist
If the SimpleFIN URL leaks, revoke it in SimpleFIN and issue a new token; your app never held bank passwords.
1. Contain the issue without destroying evidence or user data.
2. Record the timeline and affected scope.
3. Rotate exposed secrets and revoke compromised sessions or credentials.
4. Restore from a verified backup when needed.
5. Document the root cause, the remediation and the regression test.
## Release gate
- [ ] Money maths verified against a hand-built month
- [ ] Import dedupe verified on a real bank file twice
- [ ] One restore drill performed
- [ ] Sync disabled path verified
## Launch constraint
Do not market omitted YNAB capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Bound to 127.0.0.1 only.
PORT=4820
# Required. Your money. Back it up.
DATABASE_PATH=./data/budget.db
# Required. ISO code for formatting.
CURRENCY=USD
# Required. Your bank's CSV date format, asked once per account on import and stored.
DATE_FORMAT_HINT=MM/DD/YYYY
# Optional · secret. From SimpleFIN after exchanging the setup token. Empty disables sync.
SIMPLEFIN_ACCESS_URL=https://...:...@beta-bridge.simplefin.org/simplefin
# YNAB · indie build A zero-based budget you own: accounts and categories, a monthly grid where every dollar is assigned, a To Be Budgeted figure that turns red when you over-assign, CSV import with remembered column mappings and payee rules, reconciliation against the real bank balance, credit cards handled the way a budget should, and optional bank sync through SimpleFIN for about $15 a year instead of YNAB's price. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with Express and better-sqlite3 | many views and forms; the framework earns its place | | Money | Integer cents, never floats | a budget that disagrees with the bank by a cent loses all trust | | Dates | YYYY-MM-DD strings, not timestamps | a transaction happens on a date; timezone shifts move money between months | | Hosting | localhost only | this is your money; it does not need to be on the internet | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **A CSV export from each bank account** · free - Why: Phase 4 imports real data; you need a file per account to test column mapping and sign conventions. - Get it: In your bank's web app, find Export or Download transactions, choose CSV, last 90 days. Save one per account, including a credit card. - [ ] **Your category groups and categories, written down** · free - Why: The budget grid is built on them. Deciding in a spreadsheet first stops Phase 2 from becoming a planning session. - Get it: Groups like Bills, Everyday, Savings Goals; six to fifteen categories total to start. - [ ] **Today's real balance for every account** · free - Why: Reconciliation in Phase 5 needs the true figure. - Get it: Read them off the bank apps now and note the date. - [ ] **A SimpleFIN Bridge access token (optional)** (optional) · about $15 a year - Why: Automatic bank sync, the one thing CSV import cannot replace. About $15 a year, and what Actual Budget uses for the same job. You never hold bank credentials; the aggregator does. - Get it: beta-bridge.simplefin.org > sign up > connect your banks > create an access token (a setup token you exchange once for an access URL). Put the access URL in .env. ## Quick start ```sh mkdir budget && cd budget && git init && npm init -y && npm pkg set type=module npm install express@4 better-sqlite3@13 mkdir data && cp .env.example .env ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Mobile apps and family sharing beyond two logins. - The educational method and habit design, which is a real part of what YNAB sells. - Polished reports. - bank sync - mobile apps - educational method/content - family sharing - polished reports - support - habit design If one of those is essential to you, that is the reason to keep paying for YNAB, and the README should say so rather than pretend.
# Build brief · YNAB
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a zero-based budgeting app to replace YNAB. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost only.
- Money is stored as integer minor units (cents). Never a float. A budgeting app
that uses floating point will disagree with the bank by a cent and destroy the
user's trust in every number on the screen.
- Dates are plain `YYYY-MM-DD` strings, not timestamps. A transaction happens on
a date, not at an instant, and timezone-shifting one across a month boundary
moves money between budgets.
### Data model (create this before Phase 1)
- `accounts`: id, name, kind ('checking' | 'savings' | 'cash' | 'credit'),
on_budget (bool), closed (bool)
- `category_groups`: id, name, position
- `categories`: id, group_id, name, position, hidden
- `budgets`: id, month ('YYYY-MM'), category_id, assigned_cents
- `transactions`: id, account_id, date, payee, category_id (nullable),
amount_cents (negative is outflow), memo, cleared (bool), reconciled (bool),
transfer_transaction_id (nullable)
- `payee_rules`: payee_pattern, category_id
Every amount is signed from the account's perspective. A transfer is two rows
pointing at each other, not one row with two accounts · anything else makes
reconciliation impossible to reason about later.
### Phase 1 · Accounts and transactions
Build: account CRUD, and a fast keyboard-first transaction entry form (date,
payee, category, amount, memo). Running account balances computed by query, not
stored on the account row · a cached balance will drift and there is no way to
tell which number is the lie.
Done when: entering ten transactions across two accounts yields balances matching
a hand sum, editing an amount updates the balance, deleting restores it, and
entering `12.10` stores `1210` and renders `$12.10` exactly.
Do not build yet: categories, the budget, import.
### Phase 2 · Categories and the budget grid
Build: category groups and categories, then the monthly budget view · a grid of
category, assigned, activity and available. `activity` is the sum of that
category's transactions in that month. `available` is the previous month's
available plus this month's assigned plus activity, so unspent money rolls
forward.
Done when: assigning $200 to Groceries and spending $50 shows assigned 200,
activity -50, available 150; the next month opens with 150 available before
anything is assigned; and a category overspent to -20 carries the negative into
the next month rather than silently resetting.
### Phase 3 · To Be Budgeted
Build: the header figure · total inflow to on-budget accounts minus everything
assigned across all months. Show it prominently, and turn it red when negative.
This number is the entire method: every dollar has a job, and the app's job is to
tell you when one does not.
Done when: adding $1,000 of income raises it by exactly 1000, assigning $400
lowers it by 400, over-assigning turns it red with the correct negative figure,
and it reconciles with a hand-written SQL query on a seeded fixture.
### Phase 4 · CSV import
Build: import from a bank CSV. Present a column-mapping step on first import per
account (date, payee, amount, or separate debit/credit columns) and remember the
mapping. Parse dates without guessing between `DD/MM` and `MM/DD` · ask once and
store the answer, because guessing wrong silently shifts transactions by months.
Deduplicate against existing rows on date, amount and payee, and show what will
be skipped before committing. Apply `payee_rules` so repeat payees auto-fill
their category, and learn a rule whenever the user categorizes a new payee.
Done when: importing the same file twice adds nothing the second time, an
ambiguous date format is asked about rather than assumed, a credit-card CSV with
inverted signs imports with correct polarity, and a known payee arrives
pre-categorized.
### Phase 5 · Reconciliation
Build: the reconcile flow · enter the real bank balance, the app shows the
difference against the cleared balance, the user ticks transactions cleared until
it reaches zero, then locks them as reconciled. Offer to create a balance
adjustment transaction for a remaining difference rather than editing history.
Done when: a $3 discrepancy is reported precisely, ticking the missing
transaction clears it, reconciled transactions resist accidental edits, and the
adjustment path leaves an auditable row rather than a silent change.
### Phase 6 · Credit cards
Build: credit-card handling · spending in a category on a credit card moves the
assigned money to that card's payment category, so the budget shows money set
aside to pay the bill. This is the part every simple budgeting app gets wrong,
and it is the difference between a spending tracker and a budget.
Done when: a $50 grocery purchase on a credit card reduces Groceries available by
50 and increases the card's payment category by 50, and paying the card as a
transfer reduces both correctly.
### Phase 7 · Reports and backups
Build: spending by category per month and net worth over time as inline SVG or
Chart.js, plus a nightly copy of the database to `backups/budget-YYYY-MM-DD.db`
keeping 30, and a documented restore.
Done when: the report totals match the transaction table, and a restore has been
performed once for real.
### Phase 8 · Optional bank sync, honestly priced
Build: this phase is optional and you should read the numbers before starting it.
Bank aggregation is the one thing CSV import cannot replace, and it does not have
to cost what YNAB costs · SimpleFIN Bridge is roughly $15/year and is what Actual
Budget uses for the same job. Put it behind an interface with the CSV importer as
the other implementation, keep the token in `.env`, and never store bank
credentials yourself · the aggregator holds them, which is the entire reason to
use one.
Done when: a sync pulls new transactions, dedupes against manual entries, and the
app still works completely with the integration disabled and no token present.
### Out of scope (and why)
- Mobile apps and family sharing.
- The educational method and habit design, which is a real part of what YNAB
sells · the software is the smaller half of that product.
- Polished reports.
### README must contain
- The CSV column-mapping step and how to redo a mapping.
- The credit-card model explained in three sentences, because it will look wrong
to anyone expecting a spending tracker.
- The bank-sync cost, with the date checked, and a statement that it is optional.# Agent instructions · YNAB indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with Express and better-sqlite3, Integer cents, never floats, YYYY-MM-DD strings, not timestamps, localhost only. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
# Build plan · YNAB A zero-based budget you own: accounts and categories, a monthly grid where every dollar is assigned, a To Be Budgeted figure that turns red when you over-assign, CSV import with remembered column mappings and payee rules, reconciliation against the real bank balance, credit cards handled the way a budget should, and optional bank sync through SimpleFIN for about $15 a year instead of YNAB's price. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Accounts and transactions Fast keyboard entry, balances computed by query, cents stored exactly. ### Steps 1. Create the project and the tables accounts (id, name, kind, on_budget, closed), category_groups, categories, budgets (month, category_id, assigned_cents), transactions (id, account_id, date, payee, category_id, amount_cents signed, memo, cleared, reconciled, transfer_transaction_id), payee_rules. ```sh mkdir budget && cd budget && git init && npm init -y && npm pkg set type=module npm install express@4 better-sqlite3@13 mkdir data && cp .env.example .env ``` 2. Build account CRUD and the transaction entry form Keyboard-first: date, payee, category, amount, memo, enter to save. Parse 12.10 into 1210; never store a float. 3. Compute balances by query, not stored A cached balance drifts and you cannot tell which number lies. ### Done when - [ ] Ten transactions across two accounts yield balances matching a hand sum - [ ] Editing an amount updates the balance; deleting restores it - [ ] Entering 12.10 stores 1210 and renders $12.10 exactly ## Phase 2 · Categories and the budget grid Assigned, activity and available per category per month, with rollover. ### Steps 1. Build groups and categories from your list 2. Build the monthly grid activity = sum of the category's transactions that month; available = last month's available + assigned + activity. ### Done when - [ ] Assign $200 to Groceries, spend $50: assigned 200, activity -50, available 150 - [ ] Next month opens with 150 available before anything is assigned - [ ] An overspent -20 carries into the next month rather than resetting ## Phase 3 · To Be Budgeted The header number the whole method rests on. ### Steps 1. Compute inflow to on-budget accounts minus everything assigned across all months One query; reconcile it against a hand calculation on a fixture. 2. Show it in the header on every budget page, red when negative This number is the entire method: every dollar has a job, and the app's job is to tell you when one does not. ### Done when - [ ] Adding $1,000 income raises it by exactly 1000 - [ ] Assigning $400 lowers it by 400 - [ ] Over-assigning turns it red with the correct negative figure ## Phase 4 · CSV import Import any bank's CSV once you have mapped it, dedupe, and learn payees. ### Steps 1. Build the column-mapping step per account Date, payee, amount or debit/credit columns. Ask the date format once and store it; never guess between DD/MM and MM/DD. 2. Dedupe on date, amount and payee and preview what will be skipped 3. Apply payee_rules and learn a rule when you categorize a new payee ### Done when - [ ] Importing the same file twice adds nothing the second time - [ ] An ambiguous date format is asked about rather than assumed - [ ] A credit-card CSV with inverted signs imports with correct polarity - [ ] A known payee arrives pre-categorized ## Phase 5 · Reconciliation Match the bank to the cent and lock what matched. ### Steps 1. Build the reconcile flow Enter the real balance, see the difference against cleared, tick transactions cleared until zero, lock them reconciled. 2. Offer a balance adjustment transaction for a remaining difference An auditable row rather than a silent edit of history. ### Done when - [ ] A $3 discrepancy is reported precisely - [ ] Ticking the missing transaction clears it - [ ] Reconciled transactions resist accidental edits - [ ] The adjustment path leaves an auditable row ## Phase 6 · Credit cards Spending on a card moves the assigned money to the card's payment category. ### Steps 1. Implement the credit-card payment category rule A categorized purchase on a card reduces that category's available and raises the card's payment category by the same amount. 2. Treat paying the card as a transfer that reduces both Show the payment category in the grid so the money set aside for the bill is visible. ### Done when - [ ] A $50 grocery purchase on a card reduces Groceries by 50 and raises the card payment category by 50 - [ ] Paying the card as a transfer reduces both correctly ## Phase 7 · Reports and backups Spending by category and net worth, and a backup you have restored. ### Steps 1. Build the two reports as inline SVG 2. Nightly copy of the database to backups/, thirty kept, and one restore performed ```sh sqlite3 data/budget.db ".backup 'backups/budget-$(date +%F).db'" ``` ### Done when - [ ] Report totals match the transaction table - [ ] A restore has been performed once ## Phase 8 · Optional bank sync, honestly priced SimpleFIN behind an interface, with CSV as the other implementation and no bank credentials in your app. ### Steps 1. Exchange the setup token for the access URL once ```sh curl -s $(echo $SETUP_TOKEN | base64 -d) # the decoded setup token is a claim URL; POST to it once to receive the access URL ``` 2. Implement the sync source GET the access URL's /accounts, map transactions to your model, dedupe against manual entries by date, amount and payee. ### Done when - [ ] A sync pulls new transactions and dedupes against manual entries - [ ] The app works completely with sync disabled and no token present ## Not in this build - Mobile apps and family sharing beyond two logins. - The educational method and habit design, which is a real part of what YNAB sells. - Polished reports. ## After v1, if you want it - Goals per category (save X by month Y) shown in the grid - A PWA shell so the phone can add a transaction
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Bound to 127.0.0.1 only. PORT=4820 # Required. Your money. Back it up. DATABASE_PATH=./data/budget.db # Required. ISO code for formatting. CURRENCY=USD # Required. Your bank's CSV date format, asked once per account on import and stored. DATE_FORMAT_HINT=MM/DD/YYYY # Optional · secret. From SimpleFIN after exchanging the setup token. Empty disables sync. SIMPLEFIN_ACCESS_URL=https://...:...@beta-bridge.simplefin.org/simplefin
# YNAB · product brief ## Problem A zero-based budget app with accounts, categories, envelopes, and reports is very buildable; bank sync and habit coaching are the main paid value. ## Product outcome A budget two people can share with confidence: exact money maths, imports that never double, a bank sync you chose knowingly, backups encrypted off the box. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - local or hosted database - CSV import - optional bank aggregation API - reporting charts - backups ## Explicit non-goals for v1 - Mobile apps and family sharing beyond two logins. - The educational method and habit design, which is a real part of what YNAB sells. - Polished reports. - bank sync - mobile apps - educational method/content - family sharing - polished reports - support - habit design ## Success criteria - Money maths verified against a hand-built month - Import dedupe verified on a real bank file twice - One restore drill performed - Sync disabled path verified
# Build brief · YNAB
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a zero-based budgeting app to replace YNAB. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost only.
- Money is stored as integer minor units (cents). Never a float. A budgeting app
that uses floating point will disagree with the bank by a cent and destroy the
user's trust in every number on the screen.
- Dates are plain `YYYY-MM-DD` strings, not timestamps. A transaction happens on
a date, not at an instant, and timezone-shifting one across a month boundary
moves money between budgets.
### Data model (create this before Phase 1)
- `accounts`: id, name, kind ('checking' | 'savings' | 'cash' | 'credit'),
on_budget (bool), closed (bool)
- `category_groups`: id, name, position
- `categories`: id, group_id, name, position, hidden
- `budgets`: id, month ('YYYY-MM'), category_id, assigned_cents
- `transactions`: id, account_id, date, payee, category_id (nullable),
amount_cents (negative is outflow), memo, cleared (bool), reconciled (bool),
transfer_transaction_id (nullable)
- `payee_rules`: payee_pattern, category_id
Every amount is signed from the account's perspective. A transfer is two rows
pointing at each other, not one row with two accounts · anything else makes
reconciliation impossible to reason about later.
### Phase 1 · Accounts and transactions
Build: account CRUD, and a fast keyboard-first transaction entry form (date,
payee, category, amount, memo). Running account balances computed by query, not
stored on the account row · a cached balance will drift and there is no way to
tell which number is the lie.
Done when: entering ten transactions across two accounts yields balances matching
a hand sum, editing an amount updates the balance, deleting restores it, and
entering `12.10` stores `1210` and renders `$12.10` exactly.
Do not build yet: categories, the budget, import.
### Phase 2 · Categories and the budget grid
Build: category groups and categories, then the monthly budget view · a grid of
category, assigned, activity and available. `activity` is the sum of that
category's transactions in that month. `available` is the previous month's
available plus this month's assigned plus activity, so unspent money rolls
forward.
Done when: assigning $200 to Groceries and spending $50 shows assigned 200,
activity -50, available 150; the next month opens with 150 available before
anything is assigned; and a category overspent to -20 carries the negative into
the next month rather than silently resetting.
### Phase 3 · To Be Budgeted
Build: the header figure · total inflow to on-budget accounts minus everything
assigned across all months. Show it prominently, and turn it red when negative.
This number is the entire method: every dollar has a job, and the app's job is to
tell you when one does not.
Done when: adding $1,000 of income raises it by exactly 1000, assigning $400
lowers it by 400, over-assigning turns it red with the correct negative figure,
and it reconciles with a hand-written SQL query on a seeded fixture.
### Phase 4 · CSV import
Build: import from a bank CSV. Present a column-mapping step on first import per
account (date, payee, amount, or separate debit/credit columns) and remember the
mapping. Parse dates without guessing between `DD/MM` and `MM/DD` · ask once and
store the answer, because guessing wrong silently shifts transactions by months.
Deduplicate against existing rows on date, amount and payee, and show what will
be skipped before committing. Apply `payee_rules` so repeat payees auto-fill
their category, and learn a rule whenever the user categorizes a new payee.
Done when: importing the same file twice adds nothing the second time, an
ambiguous date format is asked about rather than assumed, a credit-card CSV with
inverted signs imports with correct polarity, and a known payee arrives
pre-categorized.
### Phase 5 · Reconciliation
Build: the reconcile flow · enter the real bank balance, the app shows the
difference against the cleared balance, the user ticks transactions cleared until
it reaches zero, then locks them as reconciled. Offer to create a balance
adjustment transaction for a remaining difference rather than editing history.
Done when: a $3 discrepancy is reported precisely, ticking the missing
transaction clears it, reconciled transactions resist accidental edits, and the
adjustment path leaves an auditable row rather than a silent change.
### Phase 6 · Credit cards
Build: credit-card handling · spending in a category on a credit card moves the
assigned money to that card's payment category, so the budget shows money set
aside to pay the bill. This is the part every simple budgeting app gets wrong,
and it is the difference between a spending tracker and a budget.
Done when: a $50 grocery purchase on a credit card reduces Groceries available by
50 and increases the card's payment category by 50, and paying the card as a
transfer reduces both correctly.
### Phase 7 · Reports and backups
Build: spending by category per month and net worth over time as inline SVG or
Chart.js, plus a nightly copy of the database to `backups/budget-YYYY-MM-DD.db`
keeping 30, and a documented restore.
Done when: the report totals match the transaction table, and a restore has been
performed once for real.
### Phase 8 · Optional bank sync, honestly priced
Build: this phase is optional and you should read the numbers before starting it.
Bank aggregation is the one thing CSV import cannot replace, and it does not have
to cost what YNAB costs · SimpleFIN Bridge is roughly $15/year and is what Actual
Budget uses for the same job. Put it behind an interface with the CSV importer as
the other implementation, keep the token in `.env`, and never store bank
credentials yourself · the aggregator holds them, which is the entire reason to
use one.
Done when: a sync pulls new transactions, dedupes against manual entries, and the
app still works completely with the integration disabled and no token present.
### Out of scope (and why)
- Mobile apps and family sharing.
- The educational method and habit design, which is a real part of what YNAB
sells · the software is the smaller half of that product.
- Polished reports.
### README must contain
- The CSV column-mapping step and how to redo a mapping.
- The credit-card model explained in three sentences, because it will look wrong
to anyone expecting a spending tracker.
- The bank-sync cost, with the date checked, and a statement that it is optional.# Architecture · YNAB ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with Express and better-sqlite3 | many views and forms; the framework earns its place | | Money | Integer cents, never floats | a budget that disagrees with the bank by a cent loses all trust | | Dates | YYYY-MM-DD strings, not timestamps | a transaction happens on a date; timezone shifts move money between months | | Hosting | localhost only | this is your money; it does not need to be on the internet | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Ledger | accounts, transactions, transfers, cents | The core; everything else reads it | | Budget | assigned, activity, available, TBB | Pure queries over the ledger | | Import | CSV mapping, dedupe, payee rules, and the SimpleFIN source | Add another aggregator as a second source | | Reconcile | cleared and reconciled state | Rules only; no external dependency | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Bound to 127.0.0.1 only. - `DATABASE_PATH` · required · Your money. Back it up. - `CURRENCY` · required · ISO code for formatting. - `DATE_FORMAT_HINT` · required · Your bank's CSV date format, asked once per account on import and stored. - `SIMPLEFIN_ACCESS_URL` · optional, secret · From SimpleFIN after exchanging the setup token. Empty disables sync. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · YNAB product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with Express and better-sqlite3, Integer cents, never floats, YYYY-MM-DD strings, not timestamps, localhost only. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone.
# Delivery milestones · YNAB Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Accounts and transactions Fast keyboard entry, balances computed by query, cents stored exactly. ### Steps 1. Create the project and the tables accounts (id, name, kind, on_budget, closed), category_groups, categories, budgets (month, category_id, assigned_cents), transactions (id, account_id, date, payee, category_id, amount_cents signed, memo, cleared, reconciled, transfer_transaction_id), payee_rules. ```sh mkdir budget && cd budget && git init && npm init -y && npm pkg set type=module npm install express@4 better-sqlite3@13 mkdir data && cp .env.example .env ``` 2. Build account CRUD and the transaction entry form Keyboard-first: date, payee, category, amount, memo, enter to save. Parse 12.10 into 1210; never store a float. 3. Compute balances by query, not stored A cached balance drifts and you cannot tell which number lies. ### Done when - [ ] Ten transactions across two accounts yield balances matching a hand sum - [ ] Editing an amount updates the balance; deleting restores it - [ ] Entering 12.10 stores 1210 and renders $12.10 exactly ## M2 · Categories and the budget grid Assigned, activity and available per category per month, with rollover. ### Steps 1. Build groups and categories from your list 2. Build the monthly grid activity = sum of the category's transactions that month; available = last month's available + assigned + activity. ### Done when - [ ] Assign $200 to Groceries, spend $50: assigned 200, activity -50, available 150 - [ ] Next month opens with 150 available before anything is assigned - [ ] An overspent -20 carries into the next month rather than resetting ## M3 · To Be Budgeted The header number the whole method rests on. ### Steps 1. Compute inflow to on-budget accounts minus everything assigned across all months One query; reconcile it against a hand calculation on a fixture. 2. Show it in the header on every budget page, red when negative This number is the entire method: every dollar has a job, and the app's job is to tell you when one does not. ### Done when - [ ] Adding $1,000 income raises it by exactly 1000 - [ ] Assigning $400 lowers it by 400 - [ ] Over-assigning turns it red with the correct negative figure ## M4 · CSV import Import any bank's CSV once you have mapped it, dedupe, and learn payees. ### Steps 1. Build the column-mapping step per account Date, payee, amount or debit/credit columns. Ask the date format once and store it; never guess between DD/MM and MM/DD. 2. Dedupe on date, amount and payee and preview what will be skipped 3. Apply payee_rules and learn a rule when you categorize a new payee ### Done when - [ ] Importing the same file twice adds nothing the second time - [ ] An ambiguous date format is asked about rather than assumed - [ ] A credit-card CSV with inverted signs imports with correct polarity - [ ] A known payee arrives pre-categorized ## M5 · Reconciliation Match the bank to the cent and lock what matched. ### Steps 1. Build the reconcile flow Enter the real balance, see the difference against cleared, tick transactions cleared until zero, lock them reconciled. 2. Offer a balance adjustment transaction for a remaining difference An auditable row rather than a silent edit of history. ### Done when - [ ] A $3 discrepancy is reported precisely - [ ] Ticking the missing transaction clears it - [ ] Reconciled transactions resist accidental edits - [ ] The adjustment path leaves an auditable row ## M6 · Credit cards Spending on a card moves the assigned money to the card's payment category. ### Steps 1. Implement the credit-card payment category rule A categorized purchase on a card reduces that category's available and raises the card's payment category by the same amount. 2. Treat paying the card as a transfer that reduces both Show the payment category in the grid so the money set aside for the bill is visible. ### Done when - [ ] A $50 grocery purchase on a card reduces Groceries by 50 and raises the card payment category by 50 - [ ] Paying the card as a transfer reduces both correctly ## M7 · Reports and backups Spending by category and net worth, and a backup you have restored. ### Steps 1. Build the two reports as inline SVG 2. Nightly copy of the database to backups/, thirty kept, and one restore performed ```sh sqlite3 data/budget.db ".backup 'backups/budget-$(date +%F).db'" ``` ### Done when - [ ] Report totals match the transaction table - [ ] A restore has been performed once ## M8 · Optional bank sync, honestly priced SimpleFIN behind an interface, with CSV as the other implementation and no bank credentials in your app. ### Steps 1. Exchange the setup token for the access URL once ```sh curl -s $(echo $SETUP_TOKEN | base64 -d) # the decoded setup token is a claim URL; POST to it once to receive the access URL ``` 2. Implement the sync source GET the access URL's /accounts, map transactions to your model, dedupe against manual entries by date, amount and payee. ### Done when - [ ] A sync pulls new transactions and dedupes against manual entries - [ ] The app works completely with sync disabled and no token present ## M9 · Run it on a server for two people (production only) Only if you share the budget: HTTPS, a login, off-box backups. ### Steps 1. Add a single-user login and bind behind Caddy with HTTPS 2. Nightly off-box encrypted backup with age ```sh age -r $AGE_RECIPIENT -o backups/budget-$(date +%F).db.age data/budget.db ``` 3. Uptime check and structured logs ### Done when - [ ] The login is required for every route - [ ] A restore from the encrypted backup opens correctly
# Operations · YNAB ## Backup Nightly SQLite backup, encrypted with age when off the box. ## Restore Decrypt, copy back, open; check TBB matches the last known figure. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Uptime on /healthz when hosted; a failed sync is shown in the UI. ## Incident checklist If the SimpleFIN URL leaks, revoke it in SimpleFIN and issue a new token; your app never held bank passwords. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Money maths verified against a hand-built month - [ ] Import dedupe verified on a real bank file twice - [ ] One restore drill performed - [ ] Sync disabled path verified ## Launch constraint Do not market omitted YNAB capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Bound to 127.0.0.1 only. PORT=4820 # Required. Your money. Back it up. DATABASE_PATH=./data/budget.db # Required. ISO code for formatting. CURRENCY=USD # Required. Your bank's CSV date format, asked once per account on import and stored. DATE_FORMAT_HINT=MM/DD/YYYY # Optional · secret. From SimpleFIN after exchanging the setup token. Empty disables sync. SIMPLEFIN_ACCESS_URL=https://...:...@beta-bridge.simplefin.org/simplefin
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because the method and mobile/bank-sync habit are easier to follow than a spreadsheet.
xbank sync
xmobile apps
xeducational method/content
xfamily sharing
xpolished reports
xsupport
xhabit design
Don't feel like building it? These folks already made it free.
all 3 free alternatives to YNAB →· no votes, no pay-to-list · just what's real
YNAB pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| ynab | $14.99 | $9.08 | All features; share one subscription with up to 5 other people (6 total) |
free tierno free tier; 34-day free trial; up to 6 people can share one paid subscription
billingmonthly + annual ($109/year); taxes extra where applicable
hidden costsThird-party app stores may require a card for the trial and can use different billing terms.
verified 2026-08-13 · source ↗
Vibecode YNAB
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal YNAB replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does YNAB cost?
YNAB costs about $14.99/month (Monthly Plan, checked 2026-07-30), which is $179.88 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing YNAB?
Honestly: bank sync; mobile apps; educational method/content; family sharing; polished reports; support; habit design. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to YNAB?
Yes: Actual Budget (Envelope budgeting, imports and rules on your own disk; bank sync is optional, not the landlord.) OpenBudgeteer (YNAB-style buckets in a Docker box; bank sync and habit coaching did not make the container.) Aspire Budgeting (A polished zero-based budget living in Google Sheets; CSV imports and auto-categorization are the paid shortcut.) All 3 curated free alternatives are at vibecodeit.com/ynab/alternatives. The prompt is for when you want it exactly your way.