Documentation
Everything you need to run and use getbud. For the code and issues, see GitHub — architecture, configuration, data-model, and deployment references also live in the repo as markdown under docs/.
Quick start
The repo ships a compose file with the app and Postgres:
git clone https://github.com/ctrl-research/getbud
cd getbud
cp .env.example .env # set POSTGRES_PASSWORD at minimum
docker compose up -d
The app listens on :8081 (the container listens on 8080; compose maps it
to host 8081). Prebuilt multi-arch images (amd64/arm64) are on GHCR if you'd rather
point your own deployment at them:
docker pull ghcr.io/ctrl-research/getbud:latest # or a specific X.Y.Z
Database migrations run automatically at startup. The first account to sign in becomes admin and every new user starts with a sensible set of income and expense categories.
The compose file enables local email/password auth for a quick first look — create a user with:
docker compose exec app getbud seed # [email protected] / getbud-dev
For a real deployment, configure Google or OIDC sign-in and set
GETBUD_LOCAL_AUTH=false.
Configuration
Everything is configured with GETBUD_* environment variables
(compose reads them from .env):
| Variable | Default | What it does |
|---|---|---|
GETBUD_DATABASE_URL | — | Required. Postgres connection string. |
GETBUD_ADDR | :8080 | Listen address. |
GETBUD_BASE_URL | http://localhost:8080 | Public URL of the instance. OAuth redirect URIs derive from it; session cookies are Secure when it's https. No trailing slash. |
GETBUD_GOOGLE_CLIENT_ID / GETBUD_GOOGLE_CLIENT_SECRET | — | Enable Sign in with Google (set both together). |
GETBUD_OIDC_ISSUER_URL + _CLIENT_ID / _CLIENT_SECRET | — | Generic OIDC provider (Authentik, Keycloak, …); redirect URI $BASE_URL/auth/oidc/callback. GETBUD_OIDC_NAME labels the login button. |
GETBUD_LOCAL_AUTH | false | Email/password accounts, intended for dev/testing. Create one with the seed subcommand. |
GETBUD_ALLOWED_EMAILS | — | Comma-separated emails allowed to sign up after the first user. Empty = closed instance. |
Sign-in options
- In the Google Cloud Console, create a project and configure the OAuth consent screen (External; Testing mode is fine — add the accounts that will sign in as test users).
- Create an OAuth client ID of type Web application with the
authorized redirect URI
$GETBUD_BASE_URL/auth/google/callback. - Set
GETBUD_GOOGLE_CLIENT_IDandGETBUD_GOOGLE_CLIENT_SECRETand restart. The login page shows the Google button automatically.
Generic OIDC
Any OIDC provider with discovery works (Authentik, Keycloak, Pocket ID, …). Set the
issuer URL exactly as the provider publishes it (trailing slash included), the client
id/secret, and the redirect URI $GETBUD_BASE_URL/auth/oidc/callback.
In both cases: sign in yourself first — the first account becomes admin
and bypasses the allowlist. After that, only GETBUD_ALLOWED_EMAILS addresses
can register; existing accounts always keep signing in. Each user's data is completely
private.
Accounts & balances
Accounts come in two flavours:
- Cash accounts (chequing, savings, credit card, other) derive their balance from an opening balance plus the sum of their transactions.
- Investment accounts (RRSP, TFSA, FHSA, non-registered) are tracked with balance snapshots — point-in-time balances you record from your statement. The latest snapshot is the account's balance, and the history drives the net-worth chart. No individual holdings or tickers, by design.
Accounts with transactions can't be deleted — archive them instead and they drop out of pickers while their history stays intact.
Transactions & transfers
Amounts are signed: money in is positive, money out is negative (the UI's income/expense toggle handles the sign for you). Every transaction can carry a payee, notes, and a category; categories are flat, per-user, and manageable in Settings (rename, recolor, archive, or delete with reassignment).
Transfers move money between two of your own accounts — paying a credit card, funding a TFSA. Both legs are created together, deleted together, and excluded from every income/expense report. A transfer into a registered account is what drives contribution tracking.
CSV import
Import → pick the account → drop the export from your bank. getbud sniffs the delimiter (comma, semicolon, tab), strips BOMs, detects the header row, and guesses the column mapping — date (with format detection, including day/month disambiguation), payee, and amount as either one signed column or split debit/credit columns. Credit-card exports that report purchases as positive numbers have a one-click "positive means expense" toggle.
The review step shows every row before anything is saved:
- new — will be imported;
- already imported — matches an existing transaction (same date, amount, and normalized payee) and starts unchecked; re-check it if it's a legitimate repeat purchase;
- repeated in file — a duplicate within the file itself;
- unparseable — rows that fail to parse are listed and skipped.
Duplicate detection is a soft warning, never a silent drop. Each import is recorded as a batch — the history at the bottom of the Import page reverts any batch atomically, and the mapping you used is remembered per account for next time.
Contribution room
The Contributions page tracks RRSP, TFSA, and FHSA room per calendar year. Room is yours to enter — TFSA room is cumulative and personal, RRSP room comes from your notice of assessment — so getbud shows the published annual limits only as placeholder hints (check CRA MyAccount for your real number).
Contributions and withdrawals are derived from transactions in your registered accounts: deposits count as contributions, withdrawals as withdrawals, and transfers between two accounts of the same type (TFSA → TFSA) are correctly ignored — moving money within a type isn't new room used. Cross-type transfers (chequing → TFSA) count.
Known simplification: contributions attribute to the calendar year they occur in; the RRSP first-60-days rule is not modelled.
Reports
- Cash flow (Sankey) — income categories pool into Income and fan out into expense categories, with the unspent remainder flowing to Savings (or a "From savings" inflow in an over-spent period). Small categories fold into "Other" to keep the diagram readable.
- Trends — monthly stacked bars by category, toggleable between expenses and income.
- Net worth — month-end totals by account type, carrying each account's latest snapshot forward.
- Categories — a donut plus a full table with per-category share.
All reports honour the shared date-range picker (this month, last month, year to date, last 12 months, or custom) and always exclude transfers. Reports assume a single currency; mixed-currency data is not converted.
Releases & images
Every merge to main bumps the patch version, tags the repo, and publishes
a multi-arch image (amd64/arm64) to ghcr.io/ctrl-research/getbud as
X.Y.Z and latest ([minor]/[major]
in the PR title bump higher). Curated GitHub Releases with notes — optionally with an
extra image tag like stable — are cut from the Release workflow in the
Actions tab. The running version is logged at startup.
Data & backups
All state lives in the Postgres database — accounts, transactions,
snapshots, room, users, sessions. A pg_dump is a complete backup.
Migrations run automatically on upgrade, so restoring into a newer version just
works.
Development
Requirements: Go and Node as pinned in .tool-versions (asdf/mise), and
Docker for Postgres.
make db # postgres via docker compose (host port 5433)
make seed # create the local dev user
make run # Go API on :8081 with local auth
make web # Vite dev server on :5173, proxying /api and /auth
Open http://localhost:5173 and sign in with
[email protected] / getbud-dev. After editing SQL under
internal/store/queries/, run make generate;
make test-db runs the Postgres-backed store tests. See
ARCHITECTURE.md
for the full tour.