Your favorite self-hosted finance tracker!
BetterTracker is a self-hosted, multi-user expense and income tracker. Manage multiple trackers with custom categories, payees, and recurring schedules. Share trackers publicly, monitor your finances with statistics and charts, and administrate users via a built-in admin panel with role-based permissions and audit logging. A separate Cases area adds patient case file management for medical billing (PVS), with a status workflow, submission batches, per-case-type PDF exports, archiving, and shared to-do lists per workspace.
Quick Start · GitHub Issues · Discord
| Feature | Description |
|---|---|
| Trackers | Multiple trackers with custom colors, currencies, and members |
| Transactions | Record income and expenses with categories and payees |
| Schedules | Recurring payment templates with flexible frequency settings |
| Statistics | Visual charts for monthly trends, category breakdowns, and more |
| Public Sharing | Share trackers publicly via a unique link with live statistics |
| Authentication | Email/password or custom OIDC provider |
| Admin Panel | User management, registration control, and audit logging |
| Roles | Superadmin, admin, and user roles with per-tracker permissions |
| Discord | Optional webhook notifications per tracker |
| Cases | Patient case file management for PVS medical billing, with a status workflow, submission batches, PDF exports, and archiving |
| Case To-dos | Shared, archivable to-do lists per case workspace |
docker run -d \
-p 3000:3000 \
-v ./data:/app/data \
-e BETTER_AUTH_SECRET=$(openssl rand -base64 32) \
-e BETTER_AUTH_URL=http://localhost:3000 \
--name bettertracker \
ghcr.io/pantelx/bettertracker:latestOpen http://localhost:3000. The first registered user becomes superadmin.
git clone https://github.com/panteLx/BetterTracker.git
cd BetterTracker
cp .env.example .env
# Edit .env and set BETTER_AUTH_SECRET
docker compose up -dgit clone https://github.com/panteLx/BetterTracker.git
cd BetterTracker
npm install
cp .env.example .env
npm run db:migrate
npm run devBETTER_AUTH_SECRET= # openssl rand -base64 32
BETTER_AUTH_URL=http://localhost:3000BETTER_AUTH_SECRET must be at least 32 characters. In production the app
refuses to start without it — every symmetric operation in the auth layer is
derived from it, so a shared default would be the same known value on every
install.
BETTER_AUTH_ALLOWED_HOSTS=192.168.1.50
BETTER_AUTH_TRUSTED_ORIGINS=http://192.168.1.50:3000Both default to BETTER_AUTH_URL, so a single-domain deployment doesn't need
to set them. Only add entries here if the app is also reachable through
another hostname or IP (e.g. a raw LAN IP alongside a public domain) —
anything listed is added on top of the default, not a replacement for it.
TRUSTED_PROXY_CIDRS=172.16.0.0/12 # CIDRs of the proxy in front of the appX-Forwarded-For is only trustworthy when a proxy you control wrote it. Set
this to your proxy's network to get per-client login rate limiting and real
client IPs in the audit log. Leave it unset when the app is exposed directly:
the header is then ignored rather than trusted, login attempts share one
rate-limit bucket, and audit-log IPs are recorded as unknown.
OIDC_DISPLAY_NAME=OpenID Connect
OIDC_DISCOVERY_URL=https://sso.example.com/.well-known/openid-configuration
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_SCOPES=openid,profile,emailDEFAULT_LOCALE=en-US # en-US or de-DE, used until a user picks a language
TZ=Europe/Berlin # used for date/time formattingSee .env.example for all options.
Patient case files live in their own workspaces (the same owner/admin/write/read permission model as trackers) inside the Cases area, reachable via the area switcher next to the theme toggle.
Status workflow: Needs processing → Medical controlling → Queued for PVS (optional) → Sent to PVS → Done, with a Returned action that resets a case back to Needs processing and tracks a return count.
PVS submissions: Sending case files to PVS groups them into a dated submission batch with per-case-type PDF exports. Admins can hide/unhide a batch from the submissions list without changing its case files' status or history.
Archiving: Case files can be archived instead of deleted — they disappear from the default board (and from bulk actions and status counts) but stay intact and reversible via a "Show archived" toggle, where they're read-only until restored.
To-dos: Each case workspace has its own Aufgaben/To-dos tab with any number of shared to-do lists, visible and editable by every workspace member with write access. Lists can be archived and restored the same way as case files.
Date entry: Date fields (like a case file's date of birth) accept typed shorthand — 18.11.99, 18111999, or 181199 all resolve to 18.11.1999 (day/month order follows the active locale).
npm run db:migrate # Apply migrations
npm run db:generate # Generate migrations after schema changes
npm run db:studio # Open Drizzle Studio GUIMaintainers with push access can cut a release from a clean main with:
npm run release patch # 0.1.0 -> 0.1.1
npm run release minor # 0.1.0 -> 0.2.0
npm run release major # 0.1.0 -> 1.0.0This bumps package.json/package-lock.json, commits, tags, and pushes, then
creates a GitHub release with auto-generated notes. The pushed tag triggers
container.yml, which builds and
publishes the versioned Docker image. Requires the GitHub CLI
to be installed and authenticated (gh auth login).
Images are available at ghcr.io/pantelx/bettertracker:
| Tag | Description |
|---|---|
latest |
Latest stable release |
vX.Y.Z |
Specific version |
main |
Development build (unstable) |
| Layer | Technology |
|---|---|
| Framework | Next.js 16, React 19, TypeScript |
| Database | SQLite, Drizzle ORM |
| Auth | Better Auth |
| UI | Tailwind CSS, shadcn/ui, Radix UI |
| Charts | Recharts |
GitHub Issues · Buy Me a Coffee · GitHub Sponsors
MIT License. See LICENSE for details.



