Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

279 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TreasuryOps

A personal expense tracker built as an append-only, double-entry-style ledger, where correctness of money math is the product — inspired by Firefly III. Work in progress.

pnpm workspace monorepo:

apps/api           NestJS REST API — Better Auth, PostgreSQL (Drizzle ORM), BullMQ workers, crons
apps/web           Next.js App Router frontend (SSR, server components)
packages/shared    zod schemas + types shared by both apps (single source of truth)
packages/config     shared tsconfig
apps/api/drizzle/  drizzle-kit migrations (ordered, additive-only)
infra/redis/        local Redis compose service

See docs/backend/BACKEND.md for the full target architecture, AGENTS.md for the non-negotiable engineering rules (money-handling invariants, TypeScript strictness, testing gates), and docs/IMPLEMENTATION-PLAN.md for the phased build-out. CLAUDE.md, apps/api/CLAUDE.md, and apps/web/CLAUDE.md have orientation for AI coding agents working in each package.

Features

Backend (apps/api) — feature-complete for the core ledger + several planned modules:

  • Auth — Better Auth (email/password), session cookies, AuthGuard + @CurrentUser(), per-user data isolation
  • Accounts & categories — editable category trees, archive/restore, and guarded permanent delete
  • Transactions — create (expense/income), cursor-paginated list with filters (accountId, categoryId, date range, description search), non-monetary edits (description/tags/category), reversal via compensating entries
  • Transfers — atomic two-leg transfers between accounts, group-level reversal
  • IdempotencyIdempotency-Key header support on money-writing endpoints; duplicate requests replay the original result instead of double-posting
  • Net-worth assets — loans given/taken, fixed deposits, gold/silver, manually valued investments; append-only valuation history; aggregate GET /v1/net-worth
  • CSV imports — upload, bank column-mapping presets, staged-row preview (dedupe/problem flags), per-row edits, chunked resumable commit, batch revert
  • Category rules — user-defined auto-categorization rules + suggester
  • Recurring transactions — rule CRUD + nightly materializer cron (Asia/Kolkata)
  • Reports — monthly rollups + nightly refresh cron
  • Notifications — outbox pattern, BullMQ delivery worker, circuit breaker on outbound calls, periodic sweep
  • Balance verification — weekly consistency-check cron comparing ledger vs. computed balances
  • CSV export
  • Budgets — per-category monthly budget targets with spend tracking
  • Goals — savings/funding goals with ordered funding sources
  • Credit card bills — statement cycles, reconciliation, overpayment/underpayment handling
  • Spending pattern warnings — detects and surfaces anomalous spend against recent history
  • API keys — scoped, user-issued keys (RequireScopes guard) as an alternative to session auth
  • Dashboard — aggregate summary plus current-month daily/weekly spending analytics backing the frontend's home view
  • Standardized API errors — RFC 7807 problem+json with stable code and user-facing message fields, request references, typed DomainError codes, and per-field validation errors
  • Health checks/healthz (liveness), /readyz (Postgres/Redis ping)
  • OpenAPI spec + generated clientpnpm gen:client generates apps/api/openapi.json (via @asteasolutions/zod-to-openapi) and a typed apps/web/src/lib/api/generated/schema.d.ts from it

Every money write is atomic (single PostgreSQL transaction: insert + balance update + audit entry, via the withTxn helper), and all amounts are stored as integer paise — never floats.

Frontend (apps/web) — wired up to the backend:

Real routes under app/(app)/ — dashboard (/), transactions (+ [transactionId]), add, accounts, categories, category-rules, assets (+ [assetId]), transfers, imports, export, reports, recurring, budgets, goals, bills, insights, spending-warnings, settings, more — each fetching real data through generated-client server loaders or typed client hooks under the matching features/*/ (which also covers api-keys, net-worth, and profile, surfaced inside settings/more rather than as their own top-level routes).

Installing and running

Prerequisites:

  • Node 24.18.x (see .nvmrc)
  • pnpm 10.28.1
  • Docker (for a local PostgreSQL 18 container and Redis — see below; not needed if you point DATABASE_URL/REDIS_URL at instances you already have running)

Local setup:

pnpm i
pnpm --filter @treasury-ops/shared build   # root lint/typecheck/test scripts assume this is already built

cp env.example .env
# uncomment and set POSTGRES_PASSWORD in .env (e.g. `local-dev-password`, matching .env.development.local.example below)
cp .env.development.local.example .env.development.local   # host-native `pnpm dev` overrides: localhost
                                                             # ports instead of Docker service names, since
                                                             # .env itself stays Docker-Compose-shaped

docker compose --env-file .env up -d postgres   # local Postgres 18 only, published on localhost:5433
cd infra/redis && cp .env.example .env && docker compose up -d && cd ../..   # local Redis

pnpm migrate                  # drizzle-kit migrate — applies apps/api/drizzle/*.sql
pnpm --filter @treasury-ops/api seed # optional: seeds demo accounts/transactions for local login
pnpm dev                      # runs apps/api and apps/web dev servers in parallel

Env vars are validated at boot via zod (apps/api/src/common/config/env.ts) — a missing/invalid var fails startup immediately. See env.example for the full list and notes on LAN/TLS cookie behavior.

Other useful commands (run from repo root):

pnpm lint                     # eslint across all workspaces, zero warnings allowed
pnpm typecheck                # tsc --noEmit across all workspaces, zero errors
pnpm test                     # vitest unit tests across all workspaces
pnpm test:integration         # apps/api only — spins up a real Postgres via testcontainers, one container
                               # per test file (needs a working Docker daemon, but not the `postgres`
                               # compose service above — testcontainers manages its own)
pnpm build                    # builds @treasury-ops/shared, @treasury-ops/api, @treasury-ops/web
pnpm format / format:check    # prettier
pnpm verify:migrations        # sanity-checks migration ordering/state
pnpm gen:client                # regenerates apps/api/openapi.json + apps/web's typed API client
                               # from it — run after changing any API route's zod schemas

Single-package/single-test commands:

pnpm --filter @treasury-ops/api test -- path/to/file.test.ts
pnpm --filter @treasury-ops/api lint / typecheck / dev / build
pnpm --filter @treasury-ops/web lint / typecheck / dev / build

CI (.github/workflows/ci.yml) runs, in order: linttypechecktesttest:integrationtest:e2everify:migrationsbuild → Trivy filesystem scan. Match this locally before pushing.

Docker (production-style, full stack):

docker compose --env-file .env build
docker compose --env-file .env run --rm migrate   # applies migrations, exits; gates everything else
docker compose --env-file .env up -d

Brings up migrate (one-shot) → api + workerweb, all behind an nginx reverse proxy (the only exposed container, localhost:3006). Postgres and Redis are shared infra (a separate host/container, e.g. a home-lab LXC) reached via DATABASE_URL/REDIS_URL — this stack doesn't run its own Postgres by default. For a fully local/offline run, use the throwaway postgres service defined in docker-compose.yml instead: either name it explicitly (docker compose --env-file .env up -d postgres, same as the local setup above) or set COMPOSE_PROFILES=local in .env so it's included automatically; POSTGRES_BIND_ADDR/POSTGRES_PASSWORD in env.example configure it. See docker-compose.yml and docs/DEPLOYMENT-TREASURY-OPS.md.

Current issues

The prioritized, evidence-backed ticket set is maintained in docs/plans/2026-07-24-stability-and-essentials.md. The highest-priority remaining items are complete idempotency coverage, crash-resumable imports, and notification-delivery hardening.

TODO

  • Salary/income module (SALARY-MODULE.md) — profiles, effective-dated versions, materializer, payday/proration logic, reconciliation; entirely unbuilt
  • Observability — OpenTelemetry, /metrics, explain() query-budget CI check
  • Auth hardening — passkeys, 2FA
  • Pin volatile dev dependencies (typescript, etc.) instead of "latest"/"beta", to keep fresh installs reproducible

About

TreasuryOps is a secure personal expense tracker built around an append-only ledger, ensuring every transaction remains accurate and auditable. It helps users record income and expenses, track account balances, import transaction data, generate reports, and manage recurring activity—all with reliable money calculations and built-in protection.

Topics

Resources

Stars

2 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages