Open the app
Under the hood

How BudgetDork's data actually fits together

A real relational model, not one flat spreadsheet — accounts, categories, and transactions are separate tables that reference each other, so renaming a category or fixing a payee doesn't mean touching every row that mentions it.

The full schema

Nine tables. Everything else in the app — the budget grid, every report, every chart — is a query over these.

erDiagram
    CATEGORY_GROUPS ||--o{ CATEGORIES : "groups"
    CATEGORY_GROUPS {
        string id PK
    }
    CATEGORIES {
        string id PK
        string groupId FK
    }
    ACCOUNTS ||--o{ TRANSACTIONS : "posts to"
    ACCOUNTS {
        string id PK
    }
    PAYEES ||--o{ TRANSACTIONS : "paid to"
    PAYEES {
        string id PK
        string defaultCategoryId FK
    }
    TAGS ||--o{ TRANSACTION_TAGS : "applied via"
    TAGS {
        string id PK
    }
    CATEGORIES ||--o{ TRANSACTIONS : "categorizes"
    CATEGORIES ||--o{ BUDGET_ALLOCATIONS : "assigned in"
    BUDGET_ALLOCATIONS {
        string categoryId FK
        string month PK
    }
    TRANSACTIONS ||--o{ TRANSACTIONS : "splits into"
    TRANSACTIONS ||--o{ TRANSACTION_TAGS : "tagged via"
    TRANSACTIONS {
        string id PK
        string accountId FK
        string payeeId FK
        string categoryId FK
        string transferAccountId FK
        string linkedTxnId FK
        string parentTxnId FK
    }
    TRANSACTION_TAGS {
        string transactionId FK
        string tagId FK
    }
    ACCOUNTS ||--o{ RECURRING_TEMPLATES : "scheduled on"
    CATEGORIES ||--o{ RECURRING_TEMPLATES : "categorizes"
    PAYEES ||--o{ RECURRING_TEMPLATES : "paid to"
    RECURRING_TEMPLATES ||--o{ TRANSACTIONS : "generates"
    RECURRING_TEMPLATES {
        string id PK
        string accountId FK
        string payeeId FK
        string categoryId FK
    }
        

Trimmed to keys and foreign keys — every field that actually draws a line between tables above. The full column list for each one (types, which fields are nullable, plain attributes like an account's type or a transaction's amount/date/cleared) isn't repeated here — this diagram is trimmed to just the relationships. Every id is a short prefixed string (acct_a1b2c3d4, txn_9f8e7d6c, …) generated client-side — readable in devtools, not meant to be guessable or sequential. Money is always a plain signed number (positive = inflow, negative = outflow), never cents-as-integers or a separate currency object.

accountsCash, Credit Card, Loan, Mortgage, Investment, Asset Tracking, …
categoryGroups / categoriesThe envelopes, and the groups they're organized into
payeesRemembers the last category used, for autofill
transactionsEvery register line — splits and transfers included
budgetAllocationsOne row per category per month — the "Assigned" amount
tags / transactionTagsFree-form labels that cut across categories
recurringTransactionsSchedule templates that generate real transactions when due

Two things that aren't obvious from the schema alone

An ER diagram shows what tables exist — not the rules that keep them honest. These two are worth seeing drawn out.

A transfer is two linked rows

Moving money between two of your own accounts isn't a special row type — it's an ordinary transaction on each account, cross-referenced by linkedTxnId, with opposite signs. Edit or delete one leg and the other stays in sync automatically.

flowchart LR
    A["Checking transaction
amount: -200.00
transferAccountId: Savings"]
    B["Savings transaction
amount: +200.00
transferAccountId: Checking"]
    A <-->|linkedTxnId| B
          

A split is one parent + N children

One receipt, several categories. The parent is the real register line (date, payee, total) with no category of its own; each hidden child carries its own category and amount. Balances read the parent only — category reports read the children only — so neither double-counts.

flowchart TD
    P["Parent transaction
Target · -77.00
categoryId: null"]
    C1["Child · Groceries
-45.00"]
    C2["Child · Hobbies
-20.00"]
    C3["Child · Pharmacy
-12.00"]
    P -->|parentTxnId| C1
    P -->|parentTxnId| C2
    P -->|parentTxnId| C3
          

How a dollar actually moves

The tables above, traced through one real dollar — from landing in a Cash account to rolling into next month.

flowchart TD
    Income(["Paycheck lands in Checking
uncategorized or income-tagged"]) --> RTA["Ready to Assign
(readyToAssign)"]
    RTA -->|"you type a number"| Assigned["budgetAllocations
assignedAmount"]
    Assigned --> Available["Category: Available
(rolls forward every month)"]
    Available -->|"you spend it"| Activity["Category: Activity
(categorized transactions)"]
    Activity --> NextMonth["Whatever's left rolls
into next month's Available"]
    NextMonth --> Available
        

This is the entire mechanism behind envelope budgeting — every other screen in the app (Insights, Reports, the "Settle prior months" button) is just a different lens on this same loop.