- TypeScript 57.5%
- Go 36%
- CSS 2.9%
- Dockerfile 2.2%
- JavaScript 0.7%
- Other 0.7%
| .forgejo/workflows | ||
| .github | ||
| .vite/deps | ||
| backend | ||
| frontend | ||
| .dockerignore | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
Haushaltsplan
A household chore tracker for kids with a parent confirmation flow. Children can mark tasks as done and earn coins that top up their weekly allowance. Parents confirm or reject completions through a certificate-gated view.
Features
- Child view – shows all chores for the current week, coin rewards per task, and a running allowance total
- Parent view – gated by an mTLS client certificate; confirm or reject pending completions with optional notes; manage tasks and settings
- Configurable – base allowance and coin value (€ per coin) are editable at runtime
- Persistent – SQLite database stored on a Docker volume
Tech stack
| Layer | Technology |
|---|---|
| Backend | Go 1.26 · chi router · modernc SQLite (no CGO) |
| Frontend | React 19 · TypeScript · Vite · Tailwind CSS v4 |
| Container | Docker Compose (nginx reverse proxy → Go API) |
Local development
Prerequisites
- Go ≥ 1.22
- Node.js ≥ 20
Backend
cd backend
go run .
# API available at http://localhost:8080
Frontend
cd frontend
npm install
npm run dev
# App available at http://localhost:5173
The Vite dev server proxies /api/* requests to http://localhost:8080.
Docker
docker compose up --build
The app will be available at http://localhost:8080.
The SQLite database is persisted in a named Docker volume (db-data).
docker-compose.yml includes Traefik labels for running behind a Traefik reverse proxy. This needs the external Docker network Traefik listens on (default traefik, override via TRAEFIK_NETWORK) and TRAEFIK_HOST set to the desired domain.
| Variable | Default | Description |
|---|---|---|
TRAEFIK_HOST |
haushaltsplan.example.com |
Domain Traefik routes to this app |
TRAEFIK_NETWORK |
traefik |
External Docker network Traefik listens on |
TRAEFIK_CERT_RESOLVER |
letsencrypt |
Traefik cert resolver name |
Access control (mTLS)
Instead of a PIN, the parent view is gated by a client certificate signed by a shared personal CA — see the separate mtls-ca repo for the CA, device-cert issuing script, and the actual TLS Options definition (Docker labels can only reference a tls.options entry with clientAuth settings, not define one, so it's defined via Traefik's file provider and referenced here with the @file suffix).
haushaltsplan also attaches mtls-ca's pass-client-cert middleware, which forwards the verified cert's CN to the app via the X-Forwarded-Tls-Client-Cert-Info header. The backend (backend/certauth) maps that CN against the parent_cert_cns setting (comma-separated names, matching the CN a cert was issued with in mtls-ca — ./issue-client-cert.sh "<Name>") to decide whether a request is a parent.
GET /api/me returns {"certCn": ..., "isParent": ...} for the current request's certificate. The frontend uses this to show or hide the "Eltern-Ansicht" toggle — a device without a parent cert never sees it, and can't reach parent-only actions even by calling the API directly, since those are enforced server-side.
Household-admin actions — managing tasks, confirming/rejecting/deleting completions, editing settings (including parent_cert_cns itself), marking weeks/balance paid, and loan tracking — require a parent certificate. Completing a task is the only write a child's device can make.
Bootstrapping the first parent cert: since editing parent_cert_cns is itself a parent-only action, set it once before putting the app behind Traefik in production — either during local development (go run ., which is exempt) or via a direct PUT /api/settings call before APP_ENV=production is set, e.g.:
curl -X PUT http://localhost:8080/api/settings \
-H "Content-Type: application/json" \
-d '{"parent_cert_cns": "<Name>"}'
where <Name> matches the CN the cert was issued with. Add more parents later from the Settings tab in the parent view itself.
In production (APP_ENV=production, e.g. the Docker image), any write request without a client certificate at all — no cert, or the host port reached directly instead of through Traefik's mTLS gate — gets a 403. Local dev has no Traefik in front, so it's exempt from this check.
API overview
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check ({"status":"ok","database":"connected"}, 503 if the DB ping fails) |
GET |
/api/version |
Git commit hash baked in at build time ({"version":"<hash>"}, "unknown" outside Docker) |
GET |
/api/tasks |
List all tasks |
POST |
/api/tasks |
Create task |
PUT |
/api/tasks/:id |
Update task |
DELETE |
/api/tasks/:id |
Delete task |
POST |
/api/tasks/:id/complete |
Child marks task done |
GET |
/api/completions |
List completions (filter by ?week= and ?status=) |
POST |
/api/completions/:id/confirm |
Parent confirms |
POST |
/api/completions/:id/reject |
Parent rejects |
GET |
/api/summary |
Weekly summary (coins, allowance) |
GET |
/api/settings |
Read settings |
PUT |
/api/settings |
Update settings (parent-only) |
GET |
/api/me |
Current request's client-cert identity (certCn, isParent) |
Environment variables
| Variable | Default | Description |
|---|---|---|
DB_PATH |
./haushaltsplan.db |
Path to the SQLite database file |
APP_ENV |
(unset) | Set to production to enforce the client-certificate gate (see Access control) |
Default settings (first run)
| Setting | Default | Description |
|---|---|---|
parent_cert_cns |
(empty) | Comma-separated CNs of client certs recognized as a parent |
base_allowance |
500 (= 5.00 €) |
Weekly base allowance in cents |
coin_value |
50 (= 0.50 €) |
Value of one earned coin in cents |