Personal finance dashboard. Export your transactions from your bank, drop the file in, and wastescope categorizes everything and shows you where your money goes.
Parsing and categorization is done by txcategorizer.
- Import bank exports – upload a
.csv/.txtexport from a supported bank (currently DNB and Valle). The file is parsed in the browser, and you get a preview before anything is saved. - Automatic categorization – every transaction gets a category and a type (from txcategorizer).
- Duplicate detection – rows you've already imported are flagged in the preview and skipped on insert, so re-importing overlapping exports is safe.
- Category rules – re-categorize a transaction and every other transaction from the same merchant/counterparty follows. The choice is saved as a rule and applied to future imports too.
- Dashboard – stat cards, spending per category, monthly chart, and a sortable transaction table (mobile list view on small screens).
- Filters – date range, amount range, category, merchant and type. Filters can include or exclude values (long press on mobile to exclude).
- Accounts – email/password auth with account settings, active sessions and a danger zone for wiping your data.
- Demo accounts – five public demo logins with seeded data so people can try the app without signing up.
- English and Norwegian, light and dark theme.
bank export (.csv/.txt)
│
▼
txcategorizer.processTransactions() ← runs in the browser
│ parsed + categorized rows
▼
checkDuplicateTransactions (server fn) → preview with duplicates flagged
│ user confirms
▼
importTransactions (server fn)
│ apply the user's category rules, insert with ON CONFLICT DO NOTHING
▼
PostgreSQL ──► dashboard queries (stats, charts, table) via server fns + TanStack Query
- All data access goes through TanStack Start server functions in
src/server/functions/, guarded by an auth middleware that resolves the session and scopes every query to the current user. - Deduplication is enforced by a unique index on
(user_id, date, amount, merchant, type, counterparty). - Category rules live in the
category_ruletable, keyed on(user_id, merchant, counterparty). - Demo accounts are read-only: importing, deleting, re-categorizing and account changes are blocked on the server (server functions plus a Better Auth
beforehook), not just hidden in the UI.
| Area | Used |
|---|---|
| Framework | TanStack Start (React 19, SSR, server functions), TanStack Router |
| Data fetching / state | TanStack Query, TanStack Form, TanStack Table |
| Database | PostgreSQL 17, Drizzle ORM + drizzle-kit migrations |
| Auth | Better Auth + Better Auth UI |
| UI | Tailwind CSS v4, shadcn/ui on Base UI, Recharts, Phosphor icons |
| i18n | Paraglide JS (en, no) |
| Validation | Zod |
| Tooling | Vite, React Compiler, Biome, Vitest, TypeScript, pnpm |
| Deploy | Docker + Docker Compose (Nitro node server) |
src/
routes/ file-based routes (dashboard, import, auth, settings)
components/ dashboard, import, layout and shadcn components
server/functions/ server functions (transactions, user)
server/middleware/ auth middleware
db/schema/ Drizzle tables (auth, transactions, category rules)
db/queries/ query layer used by server functions
lib/ auth config, hooks, formatting, schemas, demo account helpers
content/ UI copy, wired to Paraglide messages
messages/ translation files (en.json, no.json)
drizzle/ generated SQL migrations
scripts/ seeding and maintenance scripts
demo_data/ sample bank exports used to seed demo accounts
tests/ Vitest tests
Requires Node 22+, pnpm and Docker.
pnpm install
# start Postgres
pnpm docker:up
# configure env
cp .env.example .envFill in .env:
DATABASE_URL="postgresql://wastescope:wastescope@localhost:5432/wastescope" # matches docker-compose.yml
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET= # openssl rand -base64 32Then:
pnpm db:migrate # apply migrations
pnpm seed:demo-accounts # optional: create the demo accounts
pnpm dev # http://localhost:3000| Script | What it does |
|---|---|
pnpm dev |
Dev server on port 3000 |
pnpm build / pnpm preview |
Production build / preview it |
pnpm test |
Run Vitest |
pnpm check |
Biome lint + format check |
pnpm typecheck |
tsc --noEmit |
pnpm db:generate |
Generate a migration after changing the schema |
pnpm db:studio |
Open Drizzle Studio |
pnpm seed:transactions |
Seed fake transactions |
pnpm seed:demo-reset |
Wipe and re-seed the demo accounts |
pnpm db:clearall |
Clear the database |
docker-compose.prod.yml builds the app from the Dockerfile and runs it in order:
db– Postgresmigrate– runspnpm db:migrateseed– wipes and re-seeds the demo accounts (runs on every deploy)app– the built Nitro server on port 3000
It needs POSTGRES_PASSWORD, BETTER_AUTH_SECRET and BETTER_AUTH_URL set in the environment.
docker compose -f docker-compose.prod.yml up -d --build