Dime is a strict, minimal envelope budgeting app. Every unit gets assigned; envelopes and the Pot never go negative.
cd server && npm install && cd ..
docker-compose up
# app: http://localhost:3000Deploy (Railway): push to GitHub → create Railway project → deploy from GitHub → add PostgreSQL (sets DATABASE_URL). PORT defaults to 3000.
- “Categories” are now envelopes (e.g., Rent, Groceries, Fun, Emergency).
- Envelopes cannot go below 0.
- Pot holds all unassigned money from spendable assets.
- No direct spending from Pot; assign to envelopes first.
- Pot cannot go below 0.
- Formula:
Pot = sum(spendable asset balances) − sum(envelope allocations).
- Class:
assetordebt. - Spendable: true/false (e.g., Checking = asset+spendable; Crypto = asset+non-spendable; Credit Card = debt+spendable; Mortgage = debt+non-spendable).
- Only spendable assets contribute to the Pot.
- Balances reflect reality: debts are often negative (e.g., credit cards). No additional non-negativity rule for accounts is enforced.
- No overspending: envelope ≥ 0 at all times.
- No over-assigning: Pot ≥ 0 at all times.
- No Pot spending: spend from an envelope only.
- No over-moving: envelope → envelope transfers ≤ source balance.
- Moves are paired: account
movecreates equal & opposite entries. - Debt payments reduce spendable assets: ensure Pot covers them (de-allocate first if needed).
Income → Spendable Asset → Pot → Envelopes → Spending
- Debit card / checking spend: asset ↓, envelope ↓.
- Credit card spend: debt more negative, envelope ↓, Pot unchanged.
- Paying the card: spendable asset ↓; Pot must cover.
Credit card example (correct): Start: Card −100, Groceries 10 → spend 10 on Groceries via card → Groceries 0, Card −110, Pot unchanged.
-
Transactions
in(>0): income/depositout(<0): expense/withdrawalmove(±): transfer between accounts (paired, linked)- Spending must reference a non-Pot envelope with sufficient funds.
-
Envelope Transfers (budget only): reallocate between envelopes; Pot unchanged.
Account
{
"id": "uuid",
"user_id": "uuid",
"name": "str",
"class": "asset|debt",
"spendable": true,
"balance": 2450000,
"created_at": "ISO",
"updated_at": "ISO"
}Envelope
{ "id": "uuid", "user_id": "uuid", "name": "str", "created_at": "ISO" }Transaction
{
"id": "uuid",
"user_id": "uuid",
"account_id": "uuid",
"type": "in|out|move",
"amount": -45230,
"date": "YYYY-MM-DD",
"payee_id": null,
"envelope_id": "uuid|null",
"linked_transaction_id": null,
"created_at": "ISO",
"updated_at": "ISO"
}Envelope Transfer
{
"id": "uuid",
"user_id": "uuid",
"from_envelope_id": "uuid",
"to_envelope_id": "uuid",
"amount": 100000,
"date": "YYYY-MM-DD",
"reason": null,
"created_at": "ISO"
}- Amounts are milliunits (1/1000):
$123.45 → 123450. - IDs are UUIDs.
- Type ↔ sign:
in> 0,out< 0,movepaired. - Envelope ≥ 0, Pot ≥ 0.
- Envelope transfers ≤ source.
- Account balances = sum of their transactions; deleting one side of a
movedeletes both.
Collections:
/api/accounts, /api/envelopes, /api/transactions, /api/envelope-transfers, /api/payees
Items: /api/{resource}/{id}
Success:
{ "success": true, "data": { ... } }Error:
{ "success": false, "error": { "code": "VALIDATION_ERROR", "message": "..." } }