- Python 53.9%
- TypeScript 41.5%
- CSS 4.1%
- Dockerfile 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
A new `backup` container dumps Postgres (via docker exec in the db container), pauses the backend while snapshotting the database and uploads into an encrypted remote Borg repository, and restarts it. Restores load into a scratch database while the app is still up and only swap it in with the backend stopped. Scheduling (cron in the app timezone), retention, SSH key management, archive listing and restore are admin-managed in Settings -> Backups; the backend proxies to the agent with a shared token. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T4iwCtrKkciCSPHg61xZjc |
||
| .forgejo/workflows | ||
| backend | ||
| backup | ||
| frontend | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| docker-compose.override.yml | ||
| docker-compose.yml | ||
| README.md | ||
🏠 Home Manager
A self-hosted app for keeping on top of your house:
- Trackers — recurring upkeep with a "next due" date (water the plants every 7 days, service the AC, replace filters). One tap to mark done, editable, with a "recent activity" feed. Optional alerts when something comes due, with a configurable lead time and time of day to fire. Each tracker is either household (everyone can see and complete it) or private (only the creator); only the creator can delete it.
- History — a log of everything done to the house, big (roof replaced) and small, with cost/vendor/notes. Tap any entry to see its full details and edit it (or attach/remove receipts). (Tracker activity is kept separate, on the Trackers page.)
- Goals — one-off and repeating goals (daily / weekly / monthly). Daily goals show a week grid to tick off each day; weekly/monthly goals show done/not for the current period. Goals are owned by their creator and can be shared with other members for accountability (a sharee can see the goal and check it off; only the owner can edit, delete, or change sharing).
- To Do — simple checklists. Each list has a name and holds free-text items you can add, edit, check off, and delete; completed items drop to a struck-through "Completed" area at the bottom of the list. Lists are owned by their creator and can be private or shared — a sharee can always see the list and check items off (we record who completed each item and when), and can be granted edit rights to add/edit/remove items too. Only the owner can rename, delete, or change sharing.
- Favorites — organise your favourite things into categories (Restaurants, Films, Books, …) and add items with a name, photo, a 1-10 rating, and notes. Each category and each individual item is owned by its creator and can be shared with other members to view: share a whole category (they see every item) or just a single item (they see only that one). Only the owner can edit, delete, or change sharing.
- Dashboard — quick links to your other household apps (Plex, Jellyfin, Audiobookshelf, Home Assistant, …) with custom icon images (upload or URL), editable, plus an at-a-glance "needs attention" view.
- Alerts — per-tracker reminders delivered to each user's own channels: email, SMS (via a carrier email-to-text gateway), and mobile push (ntfy). Configure your contacts and channels under Settings. Alert times are interpreted in a household timezone that admins set under Settings.
Built to work well on mobile and desktop. Logins are OIDC via Authentik, and household members are auto-provisioned on first sign-in.
Stack
| Layer | Tech |
|---|---|
| Backend | FastAPI · SQLAlchemy 2 (async) · Postgres · PyJWT |
| Frontend | React + Vite + TypeScript · TanStack Query · react-oidc-context |
| Auth | Authentik OIDC (Bearer JWT, or forward-auth headers) |
| Deploy | Docker Compose (db + backend + frontend/nginx + Borg backup agent) |
Quick start (Docker)
cp .env.example .env
# edit .env: set POSTGRES_PASSWORD and the Authentik OIDC values
docker compose up --build
App is served at http://localhost:8080. The frontend's nginx proxies /api
to the backend, so there's a single origin to expose.
Try it without Authentik
To kick the tyres before wiring up OIDC, run in dev-auth mode — every request is treated as a local admin user:
HM_DEV_AUTH=true docker compose up --build
⚠️ Never enable
HM_DEV_AUTHin production — it disables authentication.
Local development (no Docker)
Backend (Python 3.12+):
cd backend
python -m venv .venv && source .venv/bin/activate # repo already ships a .venv
pip install -e ".[dev]"
HM_DEV_AUTH=true HM_DATABASE_URL="sqlite+aiosqlite:///./dev.db" \
uvicorn app.main:app --reload
(For Postgres locally, point HM_DATABASE_URL at a running instance and drop the
SQLite URL.)
Frontend:
cd frontend
npm install
npm run dev # http://localhost:5173, proxies /api -> http://localhost:8000
Tests / typecheck:
cd backend && pytest # API smoke tests on in-memory SQLite
cd frontend && npm run build # tsc typecheck + production build
CI/CD (Forgejo Actions → Portainer)
.forgejo/workflows/build-deploy.yml runs on every push to main (and via
manual dispatch). It:
- builds and pushes
homemanagement-backend,homemanagement-frontendandhomemanagement-backupimages togit.thenymans.com/brian, tagged both:latestand:<commit-sha>; - redeploys the Portainer stack, pinning
IMAGE_TAGto that commit's SHA and pulling the new images.
The repo's docker-compose.yml is the stack Portainer runs — it references the
registry images. Local development builds from source instead via
docker-compose.override.yml (merged automatically by docker compose up --build);
that override is never sent to Portainer.
Required Forgejo repo secrets:
| Secret | Purpose |
|---|---|
REGISTRY_USER |
Registry login user for docker push |
REGISTRY_PASSWORD |
Registry login password/token |
PORTAINER_TOKEN |
Portainer API key (X-API-Key) |
PORTAINER_STACK_ID |
ID of the existing Portainer stack to update |
PORTAINER_ENDPOINT_ID |
Portainer environment/endpoint ID |
Postgres and OIDC values (POSTGRES_PASSWORD, HM_OIDC_ISSUER, …) live as
env vars on the Portainer stack itself — the deploy job preserves them and
only overrides IMAGE_TAG, so secrets never need mirroring into CI. The runner
must use the docker label and be able to reach the registry and Portainer.
If your repo owner isn't
brian, updateNAMESPACEin the workflow andIMAGE_NAMESPACEin.env/ the Portainer stack env.
Configuring Authentik
-
In Authentik, create an OAuth2/OpenID Provider:
- Client type: Public (the SPA uses Authorization Code + PKCE).
- Redirect URI:
https://homemanager.example.com/(your app's origin, with trailing slash). - Note the Client ID and the provider's Issuer URL. The last path
segment is your Authentik application slug and must match it exactly,
e.g.
https://auth.thenymans.com/application/o/home-management/. Confirm by opening<issuer>/.well-known/openid-configuration— it must return JSON.
-
Create an Application bound to that provider.
-
(Optional) Create a group — e.g.
home-manager-admins— and add the members who should be able to manage dashboard links. Make sure your provider emits agroupsscope/claim. -
Fill in
.env:HM_OIDC_ISSUER=https://auth.example.com/application/o/home-manager/ HM_OIDC_AUDIENCE=<the Client ID> HM_ADMIN_GROUP=home-manager-admins HM_CORS_ORIGINS=https://homemanager.example.com
The SPA discovers these at runtime from /api/config, runs the OIDC code flow,
and sends the access token as a Bearer token. The backend validates the token
signature against Authentik's JWKS and creates the User row on first sight.
Admins are anyone whose token carries HM_ADMIN_GROUP in its groups claim.
Alternative: forward-auth
If you instead front the backend with an Authentik proxy/forward-auth
outpost, set HM_TRUST_FORWARD_AUTH=true. The backend will then trust the
X-authentik-* headers the outpost injects. Only do this when the outpost is the
only network path to the backend.
Notifications (tracker alerts)
Alerts are opt-in per user and per tracker:
- Per tracker (Trackers page → edit): toggle Send alerts when due and set a lead time (e.g. remind 3 days before due). Requires a repeat interval.
- Per user (Settings page): turn on Email / SMS / Push and provide each channel's contact detail. A Send test button verifies delivery.
A background scheduler notifies every user whose channels are configured when a
tracker reaches due date − lead days. Each tracker fires once per due cycle;
marking it done re-arms it.
Server settings are admin-managed in the app, not via env. An admin opens
Settings → Server notifications to turn the scheduler on/off, set the scan
interval, and configure SMTP and ntfy (secrets are write-only — they're never
returned to the browser, and a blank field leaves the stored value unchanged).
The HM_ALERTS_ENABLED / HM_SMTP_* / HM_NTFY_* env vars are only seed
defaults applied on first startup; after that the database is the source of
truth. Changes in the admin UI take effect without a restart.
Admins: the first user to sign in is automatically made an admin (so a fresh
install always has someone who can manage settings). Beyond that, membership of
HM_ADMIN_GROUP grants admin. Admin is sticky — it's never revoked on a later
login, which also protects against an OIDC token that omits the groups claim.
Channels:
- Email — standard SMTP (
HM_SMTP_*). - SMS — delivered as an email to a carrier email-to-text gateway, so it
reuses SMTP — no Twilio/etc. Each user enters their gateway address (e.g.
5551234567@vtext.comfor Verizon,@txt.att.netfor AT&T). - Push — ntfy. The admin sets the ntfy server; each user
picks a personal topic and subscribes to it in the ntfy mobile app. Choose an
unguessable topic — anyone who knows it can read the alerts. Published via
ntfy's JSON API (UTF-8, emoji-safe).
- Private ntfy servers: each user can supply their own credentials in Settings — either an access token or a username + password (HTTP basic). A user's own credentials take precedence over the admin's server-wide token. These are write-only (never returned to the browser).
Uploaded dashboard/tracker icons are stored under HM_UPLOAD_DIR (a Docker
volume in the bundled compose) and served from /api/uploads/....
Backups (Borg)
Home Manager can back itself up to a remote Borg
repository on a schedule, and restore from the UI. A separate backup
container (backup/) does the work instead of the backend, because it has to
stop and restart the backend.
A backup stops the backend (every container labelled
homemanagement.backup.stop=true) and runs pg_dump inside the db
container. It then creates an encrypted, deduplicated Borg archive of the
dump plus the uploads volume, starts the backend again, and prunes old
archives according to the retention settings. The app is unavailable while
the snapshot is taken. After the first full backup, that's usually only
seconds.
A restore can first take a safety backup of the current data (archive
homemanagement-prerestore-…). It extracts the chosen archive and loads the
dump into a scratch database while the app is still running. Only then does
it stop the backend, swap the scratch database in for the live one, and
replace the uploads directory. If the dump fails to load, live data is never
touched.
Setup (Settings → Backups, admin only):
- Enter the repository URL (e.g.
ssh://borg@nas.example.com:22/./homemanagement) and a passphrase, then save. - Generate an SSH key (or paste an existing one). Add the public key to the
backup user's
~/.ssh/authorized_keyson the repository server. Tip: prefix it withcommand="borg serve --restrict-to-path /srv/borg/homemanagement",restrictso the key can only run borg. - For a new, empty location, click Initialize repository. For an existing repository, click Test connection.
- Click Export key and store the key with the passphrase somewhere other than this server.
- Set a schedule (times are in the app's timezone) and retention, or click Back up now.
The repository server needs borg 1.x (1.2 or newer); BorgBase and rsync.net
provide it. Backup settings, job history and the SSH key live in the backup
container's own backup-state volume, not in Postgres. A restore replaces
the database, which would otherwise roll those settings back partway through
the restore.
Disaster recovery on a new server: deploy the stack and sign in (the first user becomes admin). Enter the same repository URL and passphrase. Authorize the new SSH key, or paste the old private key. Then restore the newest backup. The restored database brings back all users and settings.
⚠️ The backup container mounts
/var/run/docker.sock, which is root-equivalent access to the Docker host. It publishes no ports and only accepts requests carrying the token it shares with the backend through thebackup-agent-tokenvolume. Use one repository per install: after a crash, the agent breaks the repository's stale lock, assuming nothing else uses it.
Configuration reference
All backend settings are environment variables prefixed HM_ (see
.env.example):
| Variable | Default | Purpose |
|---|---|---|
HM_DATABASE_URL |
local Postgres | SQLAlchemy async URL |
HM_CORS_ORIGINS |
http://localhost:5173 |
Allowed SPA origins (comma-separated) |
HM_OIDC_ISSUER |
— | Authentik provider issuer URL |
HM_OIDC_AUDIENCE |
— | Authentik Client ID (expected aud) |
HM_OIDC_JWKS_URL |
derived from issuer | Override JWKS endpoint if needed |
HM_ADMIN_GROUP |
home-manager-admins |
OIDC group granting admin rights |
HM_TRUST_FORWARD_AUTH |
false |
Trust X-authentik-* proxy headers |
HM_DEV_AUTH |
false |
Dev only: bypass auth as a local admin |
HM_UPLOAD_DIR |
/data/uploads |
Where uploaded icon images are stored |
HM_ALERTS_ENABLED |
false |
Run the background tracker-alert scheduler |
HM_ALERT_INTERVAL_MINUTES |
60 |
How often to scan for due trackers |
HM_SMTP_HOST / _PORT / _USER / _PASSWORD |
— | SMTP server for email + SMS gateway |
HM_SMTP_FROM |
Home Manager <…@localhost> |
From address on outgoing mail |
HM_SMTP_STARTTLS / _SSL |
true / false |
SMTP transport security |
HM_NTFY_SERVER |
https://ntfy.sh |
ntfy server for push notifications |
HM_NTFY_TOKEN |
— | Optional bearer token for a protected ntfy |
HM_BACKUP_AGENT_URL |
http://backup:8080 |
Where the backup container listens (wiring only; backup settings are in the UI) |
HM_BACKUP_AGENT_TOKEN_FILE |
/run/backup-agent/token |
Shared-volume token the backend uses to call it |
API
Interactive docs at /docs when the backend is running. Endpoints are under
/api: events, trackers (+ POST /api/trackers/{id}/done), goals,
favorites (/categories and /items, each with /shares), dashboard-links,
and users/me.
Schema note: tables are auto-created on startup, which is ideal for a single-household self-hosted app. If you later need versioned schema changes, add Alembic — the models live in
backend/app/models.py.