budgetmate.app

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

GoGoTemplHTMXHTMXSQLiteSQLiteDocker (distroless)Docker (distroless)

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.

Dockerfile
dockerfile
# 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

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