Expenses¶
Tidemill's expense-side data model — vendors, chart of accounts, bills, expenses, bill payments. Designed to accept any accounting platform (QuickBooks Online, Xero, FreshBooks, Wave, Sage) without schema changes. Last updated: May 2026
Why expenses live in Tidemill¶
Tidemill started revenue-only. Once Stripe data was flowing, the next class of question customers asked was about the other side of the income statement: "what's our burn?", "how much runway do we have?", "what's our gross margin given AWS hosting + contractor costs?". Answering those needs an expense source. QuickBooks Online is the first integration.
Data model¶
Every table is platform-neutral. Connector-specific values live in metadata_ JSON; cross-cutting tagging dimensions (project / class / department) live in bill_line.dimensions / expense_line.dimensions JSON. The dual *_cents / *_base_cents money convention matches the revenue side.
vendor ──┐
│
account │
│ │
│ ▼
│ bill ─── bill_line (accrual payable; status: open/partial/paid/voided)
│ │
│ ▼
│ bill_payment
│
└── expense ─── expense_line (direct cash/credit purchase, no bill)
| Table | Notes |
|---|---|
vendor |
Counterparty for bills/expenses. Mirror of customer on the revenue side. |
account |
Chart of Accounts. account_type is one of the canonical enums below. Tree via parent_external_id. |
bill |
A/P document with status {open, partial, paid, voided}. |
bill_line |
Line item linking the bill to an account and (optionally) tagging dimensions. |
expense |
Direct purchase (cash/credit/check). payment_type is canonical. |
expense_line |
Line item, same shape as bill_line. |
bill_payment |
Payment applied against a bill. Used to compute open A/P balances. |
Canonical enums¶
Defined in tidemill.connectors.base:
| Enum | Values |
|---|---|
account.account_type |
expense, cogs, income, asset, liability, equity, other |
bill.status |
open, partial, paid, voided |
expense.payment_type |
cash, credit_card, check, bank_transfer, other |
Each ExpenseConnector maps its native vocabulary into these. The original native string is preserved in metadata_ (e.g. metadata_.native_account_type = "Cost of Goods Sold").
Event flow¶
Connectors emit events of the form:
vendor.created/vendor.updated/vendor.deletedaccount.created/account.updatedbill.created/bill.updated/bill.paid/bill.voidedexpense.created/expense.updated/expense.voidedbill_payment.created
Event.customer_id carries the realm ID (or equivalent tenant identifier) so events for the same accounting tenant share a Kafka partition and stay strictly ordered.
The state consumer (tidemill.state) handles every prefix above with the same INSERT … ON CONFLICT DO UPDATE pattern as the revenue side. Lines on bills/expenses are bulk-replaced (DELETE + INSERT) on every header update because most accounting APIs regenerate line IDs.
Metrics¶
Today: expenses — total / by_account_type / by_vendor / monthly series. Reads bill_line UNION ALL expense_line joined to account + vendor, summing amount_base_cents. Voided bills/expenses are excluded.
Endpoints:
GET /api/metrics/expenses?start=&end= → {total_base_cents, line_count}
GET /api/metrics/expenses/by_account_type?start=&end= → [{account_type, amount_base_cents, line_count}]
GET /api/metrics/expenses/by_vendor?start=&end= → [{vendor_name, amount_base_cents, line_count}]
GET /api/metrics/expenses/series?start=&end=&interval= → [{period, account_type, amount_base_cents}]
Future metrics planned on top of this same data:
- Burn rate — monthly total over the last
nmonths. - Gross margin — Stripe revenue (existing) − COGS-classified expenses, by month.
- Runway — cash balance ÷ trailing-3-month burn (needs a bank-balance source).
- Forecast — extrapolate recurring expense series forward.
Adding another expense source¶
See connectors.md. The work is:
- Subclass
ExpenseConnectorintidemill/connectors/<platform>/. - Implement
translate()(orfetch_and_translate()) for vendor / account / bill / expense / bill payment. - Implement the four normalize/extract methods.
- Add OAuth (or whatever auth the platform uses).
- Register with
@register("<platform>").
Schema, state handlers, expenses metric, and tests stay untouched.