Ledgerra is a self-hosted personal finance tracker built with an ASP.NET Core API and a React frontend. It is designed for homelab deployments such as Unraid and TrueNAS SCALE, keeps a clean JSON API, and covers the full personal finance workflow — accounts, transactions, budgets, savings goals, and analytics.
- Multiple accounts with types: Checking, Savings, Credit Card, Investment, and more
- Per-account balance chart with range selector
- Institution, masked account number, and icon support
- Transfer between accounts without distorting income/expense reporting
- Full transaction CRUD with inline editing
- Split transactions across multiple categories
- Recurring transaction templates with auto-generation
- Bulk actions: categorize, transfer, delete
- Advanced filters (date range, category, account, amount, type) with shareable URL state
- CSV export of filtered transactions
- Smart CSV import with field mapping
- Monthly category budgets with progress tracking
- CSV export of budget data with category detail
- Savings goals with optional deadlines and target amounts
- Progress tracking linked to actual account transfers
- Customizable widget layout (persisted per user)
- Global month selector
- Quick transaction entry from the dashboard
- Spending insights and net worth summary
- Monthly income vs. expense charts (powered by Recharts)
- AI-assisted monthly report analysis via OpenAI-compatible providers
- Full data export and restore (backup/restore flow)
- CSV transaction import with preview and conflict handling
- JWT authentication with access tokens and refresh tokens
- Personal access tokens for API access
- Profile, security, and appearance settings
- Accent color selection, density, motion, and minimal navigation preferences
- Dark mode
- Internationalization (i18n) with locale switching
- Mobile-responsive PWA with bottom navigation bar
- Onboarding checklist for new users
- Security-hardened API and frontend (audit fixes in v0.1.0)
backend/— .NET solution: API, domain, infrastructure, and testsfrontend/— React + TypeScript SPAdeploy/— deployment templates for Unraid and TrueNAS SCALEsite/— static GitHub Pages project websitedocker-compose.yml— full self-hosted stack
Run the whole development stack from the repository root:
./scripts/dev-stack.shThe script reuses an existing ledgerra-postgres container or starts
PostgreSQL with Docker Compose if needed, then starts the backend and Vite
frontend if they are not already running. Defaults:
- API:
http://127.0.0.1:5027 - Frontend:
http://127.0.0.1:5173 - PostgreSQL:
localhost:5432
docker run --name ledgerra-postgres \
-e POSTGRES_DB=ledgerra \
-e POSTGRES_USER=ledgerra \
-e POSTGRES_PASSWORD=ledgerra \
-p 5432:5432 \
-d postgres:17-alpineUse this connection string for local backend development:
ConnectionStrings__Ledgerra="Host=localhost;Port=5432;Database=ledgerra;Username=ledgerra;Password=ledgerra"Example:
cd backend
ConnectionStrings__Ledgerra="Host=localhost;Port=5432;Database=ledgerra;Username=ledgerra;Password=ledgerra" \
dotnet run --project src/Ledgerra.Api/Ledgerra.Api.csprojIf you already run PostgreSQL natively, create:
- database:
ledgerra - user:
ledgerra - password:
ledgerra
cd backend
dotnet test Ledgerra.sln
dotnet run --project src/Ledgerra.Api/Ledgerra.Api.csprojThe API listens on http://localhost:5027 when started via the default launch profile. A basic health endpoint is available at /health.
cd frontend
npm install
npm test
npm run build
npm run devA deterministic smoke suite is available for core end-to-end workflows. Each test registers its own user/workspace so data stays isolated across scenarios.
cd frontend
npm install
npx playwright install --with-deps chromium
E2E_BASE_URL=http://127.0.0.1:5173 E2E_API_BASE_URL=http://127.0.0.1:5027 npm run test:e2eThe suite stores failure artifacts (screenshots/videos/traces) and an HTML report in frontend/playwright-report/.
For local browser development, set:
VITE_API_BASE_URL=http://localhost:5027If you do not set it, the frontend assumes same-origin and keeps the /api prefix from each request path, which is the production Docker behavior.
Ledgerra supports non-browser API automation through personal access tokens (PATs). This is the recommended path for AI assistants (such as Codex) that import transactions from account reports.
- Sign in as the target Ledgerra user.
- Open
Settings > Personal access tokens. - Create a token and copy it immediately (it is only shown once).
Use it as a Bearer token in all API calls:
export LEDGERRA_BASE_URL="http://127.0.0.1:5027"
export LEDGERRA_PAT="<paste-token-once>"curl -sS "$LEDGERRA_BASE_URL/api/accounts" \
-H "Authorization: Bearer $LEDGERRA_PAT" \
-H "Accept: application/json"Pick the target id for the account that should receive imported transactions.
POST /api/imports/monthly-report/analyze accepts multipart/form-data with:
accountId(required)month(required,YYYY-MM)provider(required; supported values:OpenAi,Anthropic,OpenAiCompatible)file(required, CSV or PDF)
Example:
curl -sS "$LEDGERRA_BASE_URL/api/imports/monthly-report/analyze" \
-H "Authorization: Bearer $LEDGERRA_PAT" \
-F "accountId=<account-guid>" \
-F "month=2026-04" \
-F "provider=OpenAi" \
-F "file=@./statement.csv;type=text/csv"The analyze response includes enriched draft rows with signals for:
- probable duplicates
- confidence/validation warnings
- selected-by-default hints
Automation should only commit drafts that the user explicitly accepts:
- always analyze first and inspect each draft
- block or require explicit user confirmation for:
- likely duplicate rows
- low-confidence AI extraction/categorization
- rows with warnings or validation issues
- keep
sourceIdstable/unique per analyzed row - only include user-approved rows in commit
Send accepted rows to POST /api/imports/monthly-report/commit:
curl -sS "$LEDGERRA_BASE_URL/api/imports/monthly-report/commit" \
-H "Authorization: Bearer $LEDGERRA_PAT" \
-H "Content-Type: application/json" \
-d '{
"transactions": [
{
"sourceId": "row-1",
"accountId": "<account-guid>",
"categoryId": "<category-guid>",
"amount": 42.17,
"type": "Expense",
"occurredOnUtc": "2026-04-10T12:00:00Z",
"note": "Imported: Market purchase"
}
],
"acceptedDuplicateSourceIds": []
}'401 Unauthorized: missing/invalid/revoked PAT.400 Bad Request: malformed payload, empty/duplicate source ids, invalid duplicate-accept lists, or validation failures.404 Not Found: account/category identifiers not owned by the token user.- commit is all-or-nothing: invalid drafts do not partially save.
The site/ directory contains a static project website for GitHub Pages. It is
separate from the Ledgerra application and only describes the project, features,
stack, and self-hosting workflow. The included GitHub Actions workflow publishes
that static site after changes are merged to main.
In the repository settings, set Pages > Build and deployment > Source to GitHub Actions.
The included compose file is suitable for Docker or Unraid-style deployments:
docker compose up --build -dServices:
postgresstores the application databackendruns the ASP.NET Core API on internal port8080frontendserves the React app on host port8080and proxies/apito the backend
Before production use, change:
Auth__SigningKeyPOSTGRES_PASSWORD- any public-facing reverse proxy or TLS configuration
Tagged releases publish ready-to-run Docker images to GitHub Container Registry and attach deployment assets to the GitHub Release. This is the recommended path for Unraid OS, TrueNAS SCALE, and Linux hosts because the target machine only needs Docker Compose and does not need to build Ledgerra from source.
To publish a release, create and push a semantic version tag:
git tag v0.12.0
git push origin v0.12.0The release workflow runs backend and frontend checks, builds multi-architecture
Linux images for amd64 and arm64, pushes them to GHCR, and creates a GitHub
Release. The published image names are:
ghcr.io/<github-owner>/ledgerra-backend:<tag>
ghcr.io/<github-owner>/ledgerra-frontend:<tag>
The same images are also tagged as latest.
For release deployment, download these assets from the matching GitHub Release:
docker-compose.ymlenv.exampleunraid-ledgerra-stack.ymlandunraid-ledgerra-stack.env.examplefor Unraid Docker Compose Managertruenas-ledgerra-custom-app.yamlandtruenas-app-metadata.yamlfor TrueNAS SCALE custom app installsunraid-README.mdandtruenas-README.mdfor platform-specific steps
Then prepare the host:
mkdir -p ledgerra
cd ledgerra
cp env.example .envEdit .env before first start:
- set
POSTGRES_PASSWORDto a long random password - set
AUTH_SIGNING_KEYto at least 32 random characters - confirm
LEDGERRA_VERSIONmatches the release tag - change
LEDGERRA_HTTP_PORTif the host already uses port8080 - change
LEDGERRA_POSTGRES_DATAto a NAS-friendly appdata path if needed
Start Ledgerra:
docker compose pull
docker compose up -dUpgrade to a newer release by changing LEDGERRA_VERSION in .env, then run:
docker compose pull
docker compose up -dPlatform-specific deployment templates live in:
deploy/unraid/deploy/truenas/
Ledgerra can run on Unraid with the Docker Compose Manager plugin or from the Unraid terminal with Docker Compose.
-
Install the Docker Compose Manager plugin from Unraid Community Applications, or confirm that
docker composeis available in the Unraid terminal. -
Create an application directory:
mkdir -p /mnt/user/appdata/ledgerra/postgres
-
Download the release
docker-compose.ymlandenv.examplefrom the GitHub Release, then place them in/mnt/user/appdata/ledgerra. Renameenv.exampleto.env. -
Edit
/mnt/user/appdata/ledgerra/.envbefore first start:- Change
POSTGRES_PASSWORDin.env. - Replace
AUTH_SIGNING_KEYin.envwith a long random secret. - Change
LEDGERRA_HTTP_PORTif Unraid already uses8080. - Set
LEDGERRA_POSTGRES_DATA=/mnt/user/appdata/ledgerra/postgresfor Unraid-friendly backups.
- Change
-
Start Ledgerra:
cd /mnt/user/appdata/ledgerra docker compose pull docker compose up -d -
Open Ledgerra at
http://<unraid-server-ip>:8080, or the alternate host port you configured.
Useful maintenance commands:
cd /mnt/user/appdata/ledgerra
docker compose pull
docker compose up -d
docker compose logs -f
docker compose downKeep /mnt/user/appdata/ledgerra/postgres in your Unraid backup plan. That
directory contains the PostgreSQL data when using the recommended bind mount.
TrueNAS SCALE 24.10 and newer can install Ledgerra through the custom app YAML flow:
- Create a PostgreSQL data dataset such as
/mnt/tank/apps/ledgerra/postgres. - Open Apps > Discover Apps > more_vert > Install via YAML.
- Use
ledgerraas the app name. - Paste the release
truenas-ledgerra-custom-app.yamlinto the custom config editor. - Replace the example password, signing key, release tag, and dataset path.
- Save the app and open Ledgerra at
http://<truenas-ip>:8080.
Backend checks:
cd backend
dotnet test tests/Ledgerra.Domain.Tests/Ledgerra.Domain.Tests.csproj
dotnet test tests/Ledgerra.Api.Tests/Ledgerra.Api.Tests.csprojFrontend checks:
cd frontend
npm test
npm run build