BudgetMate
Family finance for Indian households - Go, Templ and HTMX in a distroless container. Zero maintenance, zero framework, zero data leaving the house.
- Timeline
- 2025
- Role
- Solo build
- Team
- Solo
- Status
- shipped
Technology Stack
✦Overview
BudgetMate is a family finance dashboard built on one uncomfortable premise: the tools that manage your money should not need the internet, a subscription, or a frontend framework's worth of maintenance. It is a ledger - so it is built like one.
The whole app compiles to a single static Go binary and ships in a distroless container. Your bank data is parsed in memory and never leaves your hardware. It runs the same on a ₹500 Raspberry Pi in a cupboard as it does on a cloud VM - zero data leaving the house is the feature, not a footnote.
✦Key Features
One static binary, zero maintenance - The entire app is a single self-contained Go executable - no runtime, no node_modules, no database server to babysit. Shipped at v1.0.0 and production-ready.
Zero-knowledge CSV import - Bank statements are parsed entirely in memory and mapped to transactions - the raw file is never persisted. Your statement never becomes a liability sitting in a database.
Hypermedia UI, no framework - Templ + HTMX give an SPA feel with smooth transitions, but endpoints return HTML fragments (HATEOAS) - no React, no bundler, perfect SEO and a tiny payload.
Calm AI nudges - Financial insights that respect attention instead of gamifying anxiety - nudges, not notifications-as-slot-machine.
✦Technical Implementation
01Stack & architecture
Go 1.22 for compiled performance and a single executable. SQLite through the pure-Go modernc driver - zero CGO, so cross-compilation is trivial. Vertical-slice architecture: each feature owns its logic and its Templ views, so a change stays local instead of rippling across layers. Every one of these calls is documented in an ADR, not folklore.
02Distroless container
Only the compiled binary ships - no shell, no package manager, no OS userland. That is roughly a 95% smaller attack surface than a typical node/python image, and nothing to patch on a Tuesday.
# build a fully static binary
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /budgetmate ./cmd/web
# ship only the binary — no shell, no package manager
FROM gcr.io/distroless/static-debian12
COPY --from=build /budgetmate /budgetmate
USER nonroot:nonroot
ENTRYPOINT ["/budgetmate"]✦Project Structure
budgetmate/
├─ cmd/web/ # main() — wires the router, one binary
├─ internal/
│ ├─ budget/ # vertical slice: logic + Templ views
│ ├─ ledger/ # transactions, CSV import (in-memory)
│ ├─ insights/ # calm AI nudges
│ └─ store/ # SQLite (modernc, pure-Go, no CGO)
├─ web/ # Templ components + HTMX fragments
├─ Dockerfile # distroless, static binary
└─ docs/adr/ # architecture decision records✦Design Decisions
HTMX over a SPA framework
For a ledger, a React SPA is maintenance debt: a build pipeline, a bundle, and hydration for pages that are 90% forms and tables. HTMX returns HTML fragments over the wire - the server stays the source of truth, and there is no JS framework to keep upgrading.
Pure-Go SQLite (modernc), no CGO
CGO makes cross-compilation painful and ties builds to a C toolchain. The modernc SQLite driver is pure Go, so a single `go build` produces a static binary for any target - Pi or cloud - with no external database process.
Local-first, in-memory parsing
The privacy guarantee is architectural: statements are parsed in memory and discarded, and the whole app runs on the user's own hardware. There is no server to breach because there is no shared server.
✦Challenges
SPA feel without a SPA
Getting smooth, app-like transitions out of server-rendered fragments took care with HTMX swaps and Templ component boundaries - the reward is an interface that feels modern with none of the client-side machinery.
Trustworthy CSV import
Bank exports are inconsistent and messy. Parsing them reliably in memory - without ever writing the raw file to disk - meant building a tolerant column-mapping step that fails loudly rather than silently corrupting the ledger.
Want the full walkthrough - architecture, decisions, war stories?
ask me about it
