- Go 1.24+
- PostgreSQL 16+
- Docker (optional, for containerized Postgres)
# Start Postgres (skip if you have a local instance)
make db
# Set DATABASE_URL (or add to .local.env — auto-loaded by Make)
export DATABASE_URL=postgres://breadbox:breadbox@localhost:5432/breadbox?sslmode=disable
# Run the dev server (auto-installs sqlc + tailwind, generates code, runs migrations, starts server)
make devOn first run, make dev downloads tailwindcss-extra, installs sqlc via go install, generates all build artifacts, runs database migrations, and starts the server. Subsequent runs skip the install/generate steps if artifacts already exist.
PostgreSQL credentials: breadbox:breadbox.
- Dev database:
postgres://breadbox:breadbox@localhost:5432/breadbox?sslmode=disable - Test database:
postgres://breadbox:breadbox@localhost:5432/breadbox_test?sslmode=disable - Migrations: Run automatically on
breadbox servestartup. For manual control:make migrate-up/make migrate-down.
Unit tests (no DB required):
go test ./...Integration tests (requires PostgreSQL):
make test-integrationIntegration test files must have //go:build integration at the top. This ensures go test ./... only runs unit tests.
- Add a
TestMaincallingtestutil.RunWithDB(m)to your package - Use
testutil.Pool(t)ortestutil.Queries(t)in tests - Use fixture helpers:
testutil.MustCreateUser,MustCreateConnection,MustCreateAccount,MustCreateTransaction - Do NOT use
t.Parallel()— tests share a database - Tables are truncated between tests automatically
See internal/service/integration_test.go for examples.
Always add integration tests for new service layer methods and API endpoints. Prefer testing through the service layer rather than HTTP handlers.
New migrations use YYYYMMDDHHMMSS_description.sql format:
# Generate a timestamp prefix
date -u +%Y%m%d%H%M%S
# e.g., 20260325153000After adding a migration:
sqlc generate # Regenerate Go code
go build ./... # Verify it compilesPL/pgSQL functions: Wrap CREATE FUNCTION ... $$ ... $$ blocks in -- +goose StatementBegin / -- +goose StatementEnd.
One HTTP server (breadbox serve) hosts everything:
- REST API at
/api/v1/... - MCP server at
/mcp - Admin dashboard at
/... - Webhooks at
/webhooks/:provider
Key packages:
| Package | Role |
|---|---|
cmd/breadbox/ |
CLI entrypoint |
internal/service/ |
Business logic (shared by API, MCP, dashboard) |
internal/api/ |
REST API handlers |
internal/mcp/ |
MCP server and tool definitions |
internal/admin/ |
Dashboard handlers and templates |
internal/provider/ |
Bank data provider interface + implementations |
internal/sync/ |
Sync engine (cron + on-demand) |
internal/db/ |
sqlc-generated database code |
The service layer is the shared core. REST handlers, MCP tools, and dashboard handlers all call service methods — they don't go through HTTP.
DaisyUI 5 + Tailwind CSS v4 via tailwindcss-extra standalone CLI (no Node.js).
make css # Compile input.css -> static/css/styles.css
make css-watch # Watch mode for developmentDefault to a single PR per change. Use stacked PRs (via Graphite) only when the change is large and splits cleanly — e.g., touches >1 layer, exceeds ~400 LOC, or has pieces that can merge in stages. Don't stack a single bug fix or UI tweak.
When stacking:
npm install -g @withgraphite/graphite-cli@stable
gt repo init --trunk main # one-time, per clone
gt sync # before starting
gt create -b stack/<topic>/01-<slug> -am "..."
# ...repeat gt create for each subsequent PR...
gt submit --stack --no-interactive # push and open/update PRs
gt land # merge bottom PR, auto-restacks the restNever git push, git checkout -b, or git commit --amend on a stacked branch — it desyncs gt's metadata. See docs/stacked-prs.md for the full policy.
- Error codes:
UPPER_SNAKE_CASEin JSON envelope{ "error": { "code": "...", "message": "..." } } - Amount convention: positive = money out (debits), negative = money in (credits)
- Category slugs:
lowercase_with_underscores(e.g.,food_and_drink_groceries) - API key format:
bb_prefix + base62 body
AGPL 3.0. See LICENSE.