A calm, self-hosted household maintenance PWA for couples and families. Every recurring chore has a frequency, and HomeKeep spreads the year's work evenly across weeks so nothing piles up and nothing rots.
A small weekend project that grew into a full v1. Existing task apps (Apple Reminders, Todoist) treat a task due in 365 days the same as one due today — everything lives in the same list, so you either ignore it or get overwhelmed. HomeKeep separates what's due now from what's coming eventually, and turns the whole year of home maintenance into a steady rhythm instead of a guilt pile.
Built for people who self-host things and want ownership of their data. AGPL v3, public repo, no cloud dependencies, no telemetry, no paid APIs.
- Calm over urgent. Reduces anxiety, not creates it. No red badges on things that aren't actually overdue.
- Shared, not competitive. Streaks and progress are "us vs. the house," never partner-vs-partner.
- Forgiveness built in. Miss a week? The app redistributes, doesn't scold.
Lightweight patch on top of v1.2.0. Addresses four small-but-noisy issues a fresh live-smoke test against a deployed instance turned up:
- Tap-to-view, not tap-to-complete. Tapping a task row now opens the detail sheet. Completion lives behind the sheet's Complete button. iOS-style tap-to-view — fewer accidental completions, and "Reschedule" / "Edit" / "Archive" are right there.
- "Keep me signed in for 14 days" checkbox on the login form. Default checked (14-day persistent cookie, prior behavior). Unchecked = session cookie that drops on browser close — useful on shared devices.
- No more warning on brand-new tasks. The early-completion guard used to fire on any completion of a task created less than 25% of its frequency ago — so "I just added this 5 minutes ago and I'm doing it today" always triggered a confirm. Now the guard only protects against genuine double-tap / re-complete accidents.
- Configurable password strength. New
PASSWORD_POLICY=simple|strongenv var. Defaultsimple= 8-char floor everywhere (right for LAN / Tailscale / single-household self-hosts). Public-facing operators setstrongfor a 12-char floor on signup + reset. Login always accepts 8+ so pre-flip accounts never get locked out. Seedocs/deployment-hardening.mditem 12b.
Plus: fixed a silent Secure cookie bug on LAN-HTTP deploys (signup succeeded but every authed page bounced to login), routed Next.js API paths correctly behind the built-in Caddy edge, and removed a dead "Learn more" link that was spamming 9+ console 404s per HTTP session.
No new user-facing features; every change tightens the attack surface or the operator-facing security posture. Ship-safe for public internet exposure (with docs/deployment-hardening.md).
- Cosign-signed images. Every
ghcr.io/the-kizz/homekeep:*tag is signed via GitHub OIDC keyless. No long-lived keys; the signature is bound to the exact workflow that built it. Verify with:cosign verify ghcr.io/the-kizz/homekeep:latest \ --certificate-identity-regexp '^https://github.com/the-kizz/homekeep/.github/workflows/release.yml@.+' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com - SBOM + SLSA-3 provenance. Every image has a SPDX SBOM (currently ~552 packages) and a SLSA-3 provenance attestation embedded as OCI attestations. Inspect with
docker buildx imagetools inspect ghcr.io/the-kizz/homekeep:latest. - Security headers + CSP. CSP-Report-Only during the soak window, HSTS on HTTPS deploys, X-Frame DENY, X-Content-Type nosniff, strict-origin-when-cross-origin referrer, permissions-policy all-off. Served at both Next.js (
next.config.ts) and Caddy (docker/Caddyfile*) layers — strip-safe. - PocketBase admin blocked at the edge.
/_/*,/api/_superusers, and related paths return 404 through Caddy by default. Operators with admin need a temporaryALLOW_PUBLIC_ADMIN_UI=trueflip or adocker execinto the container. - Row quotas + rate limits.
MAX_HOMES_PER_OWNER(5),MAX_TASKS_PER_HOME(500 active),MAX_AREAS_PER_HOME(10). Signup capped 10/60s per IP; password-reset 5/60s; auth-with-password 20/60s; invite-accept 5/60s per IP with a 3-strike per-token lockout. - SECURITY.md + disclosure process. Scope, safe-harbor, 7-day ack / 90-day fix SLA,
docs/deployment-hardening.mdchecklist (15+ items).
See docs/deployment-hardening.md for the operator checklist before public exposure.
The big idea: spread the year's work evenly across weeks. v1.0 kept tasks separate by band; v1.1 makes the app actively smooth household load so you don't get six annual tasks all landing on the same Saturday.
A per-month density tint on the Horizon strip shows you, at a glance, which months are heavy and which are light. Any task that the smoother shifted off its natural date wears a ⚖️ badge — tap the task to see Ideal: Apr 25 / Scheduled: Apr 28 / Shifted by 3 days. Nothing silent, nothing magic.
"Just this time" writes a one-off snooze that auto-consumes when you next complete the task. "From now on" permanently shifts the task's anchor / smoothed date and is preserved across future rebalances (it won't undo your intent).
Not everything is recurring. Toggle the form to One-off for single-shot tasks with an explicit "Do by" date — they auto-archive on completion, contribute to the load map while they're pending, and never clog the cycle tasks list.
Set active months on any task (e.g. October → March for winter tasks). Out of season, tasks render dimmed with "Sleeps until Mar 2027" — they don't nag, don't drag the coverage ring, and wake up automatically on the first day of their window.
When life drifts and your schedule gets lumpy, Settings → Scheduling → Rebalance schedule. Preview first ("Will update: 16 / Will preserve: 1 anchored") — Apply re-places everything eligible while respecting anchored tasks, active snoozes, and "From now on" intent.
By Area, Person, History, coverage ring, early-completion guard, cascading assignment, seed library (now with 4 seasonal pairs), ntfy notifications, mobile PWA. Nothing removed. v1.0 data migrates forward with zero changes; anchored-mode tasks are byte-identical.
- Three-band dashboard (Overdue / This Week / Horizon) + household coverage ring
- Load-smoothed scheduling (v1.1) —
placeNextDuealgorithm withmin(0.15 × frequency, 5)tolerance window, forward-only, deterministic tiebreakers, <100ms budget for 100-task households - Six-branch
computeNextDue: archived → override → smoothed → seasonal-dormant → seasonal-wakeup → one-off → cycle/anchored - Cycle vs. anchored scheduling per task (cleaning benches resets the cycle; annual smoke alarm test sticks to its fixed calendar — anchored bypasses smoothing by design)
- One-off tasks (v1.1) with explicit due date, atomic archive-on-completion
- Seasonal dormancy (v1.1) with cross-year-wrap support (Oct→Mar windows) and coverage-ring exclusion
- Snooze + permanent reschedule (v1.1) — action sheet from any task row; history-preserving
schedule_overridescollection; cross-season snooze warns with ExtendWindowDialog - Preferred days (v1.1) — optional
any / weekend / weekdayhard constraint; smoother narrows candidates before scoring by load - Manual rebalance (v1.1) — counts-only preview + 4-bucket preservation (anchored / active-snooze / from-now-on / rebalanceable); idempotent after first run
- Early-completion guard — prompts "Are you sure?" under 25% of cycle
- Collaboration — invite links, member management, cascading assignment (task-level > area-default > "Anyone")
- First-run onboarding wizard — ~30 seed tasks including 4 seasonal pairs (warm/cool mowing, summer/winter HVAC)
- Gentle gamification — household streak, per-area coverage %, area-100% celebration, "most neglected" nudge
- Push notifications via ntfy — overdue, newly-assigned, partner-completed (opt-in), weekly summary (opt-in)
- Installable PWA on HTTPS deployments; graceful HTTP degradation on LAN-only
- Append-only completion history — nothing is ever deleted; dormant-task completions still show in History regardless of current season state
- Next.js 16 (App Router, Server Components, Server Actions) + React 19
- PocketBase 0.37 (SQLite + migrations-as-code + JSVM hooks) — single binary, lives in the same container
- Tailwind 4 + shadcn/ui — soft neutrals, one warm accent (#D4A574 terracotta-sand)
- Zod + react-hook-form, @dnd-kit for drag-to-reorder
- node-cron for the hourly scheduler, ntfy for push
- s6-overlay supervises Caddy + PocketBase + Next.js inside one container
- Vitest (unit) + Playwright (E2E); 678 unit + 24 E2E tests (v1.2.1)
- Cosign keyless image signing + SPDX SBOM + SLSA-3 provenance on every GHCR push
docker run -d -p 3000:3000 \
-v homekeep_data:/app/data \
-e SITE_URL=http://localhost:3000 \
-e NTFY_URL=https://ntfy.sh \
--name homekeep --restart unless-stopped \
ghcr.io/the-kizz/homekeep:latestOpen http://localhost:3000, sign up, create a home. The PocketBase admin UI lives at /_/ — on first boot check the container logs for an installer link.
| Tag | What it is | Good for |
|---|---|---|
:latest |
Most recent stable release. No RCs, no betas. | Default — what you want unless you know otherwise. |
:rc |
Most recent release candidate. Moving pointer. | Early-access; might break. |
:edge |
Auto-built from every push to master. Bleeding edge. |
Trying the unreleased HEAD; expect breakage. |
:1 |
Latest patch of major version 1. Auto-updates within 1.x.x. |
Conservative auto-updates (bugfixes only). |
:1.0 |
Latest patch of 1.0.x. Never crosses minor. |
Very conservative — only 1.0.<next> patches. |
:1.0.0, :1.0.0-rc1, … |
Exact version pin. Never changes. | Production; reproducibility. |
Model borrowed from Plex, Nextcloud, Grafana, Postgres. :latest is stable, not nightly.
Drop this file anywhere as docker-compose.yml — no clone required:
services:
homekeep:
image: ghcr.io/the-kizz/homekeep:latest
container_name: homekeep
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- homekeep_data:/app/data
environment:
SITE_URL: http://localhost:3000
NTFY_URL: https://ntfy.sh
TZ: Australia/Perth
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/api/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
volumes:
homekeep_data:Then docker compose up -d. Data lives in the homekeep_data named volume — survives restarts and upgrades.
Or clone the repo if you want the full LAN/Caddy/Tailscale overlay set:
git clone https://github.com/the-kizz/homekeep.git
cd homekeep
cp .env.example docker/.env # edit as needed
docker compose -f docker/docker-compose.yml up -dPoint a domain at your server and:
export DOMAIN=homekeep.example.com
docker compose \
-f docker/docker-compose.yml \
-f docker/docker-compose.caddy.yml \
up -dCaddy handles TLS automatically via Let's Encrypt. See docs/deployment.md for tuning, or docker/docker-compose.tailscale.yml for the Tailscale-funnel variant.
Minimum:
| Var | Required | Default | Notes |
|---|---|---|---|
SITE_URL |
yes | — | Absolute URL (e.g. https://homekeep.example.com). Used in invite links. |
NTFY_URL |
no | https://ntfy.sh |
Override for self-hosted ntfy. |
PB_ADMIN_EMAIL |
yes for invites | — | PocketBase superuser — the invite-acceptance path needs admin context. |
PB_ADMIN_PASSWORD |
yes for invites | — | Paired with above. Create with docker exec <container> pocketbase superuser upsert <email> <pass>. |
ADMIN_SCHEDULER_TOKEN |
no | — | 32+ char string. Lets you manually trigger the scheduler via POST /api/admin/run-scheduler. |
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS |
no | — | Enables password-reset + (future) email notifications. If unset, password reset no-ops gracefully. |
HOST_PORT |
no | 3000 |
Host-side port when using docker-compose. |
TZ |
no | Etc/UTC |
Host timezone. Per-home timezone still comes from the home record. |
Full reference in .env.example.
Single Docker image. Inside the container:
┌─────────────────────────────────────────────┐
│ s6-overlay (PID 1) │
│ ├── Caddy :3000 (path-based router) │
│ │ ├─ /api/health → Next.js │
│ │ ├─ /api/* + /_/* → PocketBase │
│ │ └─ everything else → Next.js │
│ ├── Next.js :3001 (standalone, loopback) │
│ └── PocketBase :8090 (loopback) │
│ ├─ /app/pb_migrations (schema) │
│ └─ /app/pb_hooks (JSVM lifecycle) │
│ /app/data → persistent volume │
└─────────────────────────────────────────────┘
Read more in docs/deployment.md and the per-phase summaries in .planning/phases/.
npm install
npm run dev # Next.js + PocketBase side-by-side
npm test # Vitest
npm run test:e2e # Playwright (boots a disposable PB)
npm run build # production build (uses webpack for Serwist)
npm run lint && npm run type-checkPocketBase runs as a local binary under ./.pb/pocketbase via scripts/dev-pb.js. Migrations live in pocketbase/pb_migrations/, hooks in pocketbase/pb_hooks/.
v1.0.0-rc1. All 7 planned phases shipped:
| Phase | What it delivered |
|---|---|
| 1 | Docker + Next + PocketBase + Caddy + s6 + multi-arch CI |
| 2 | Signup, homes, areas, tasks, computed next-due |
| 3 | Three-band dashboard + one-tap complete + early-completion guard + coverage ring |
| 4 | Invite links + members + cascading assignment |
| 5 | By Area / Person / History views + onboarding wizard |
| 6 | ntfy notifications + scheduler + streaks + celebrations |
| 7 | PWA manifest + service worker + HTTP banner + Caddy/Tailscale compose overlays |
Decimal phases (2.1, 3.1, …) are deploy checkpoints — build the image and stand it up on the VPS between features so you can actually look at the thing.
- Password-reset emails only work if you configure SMTP.
- PWA install prompts only appear on HTTPS deployments (browser restriction — that's why the HTTP banner exists).
- Multi-instance deploys aren't supported yet — the scheduler assumes one container.
- Offline-writes is not in v1. You can read cached pages offline, but edits require a connection.
This is a small fun project, not a startup. PRs welcome — keep it calm and read CONTRIBUTING.md first. Open an issue before any big change so we don't duplicate effort.
If the app helps you keep your house, let me know — no tracking, no analytics, so the only feedback loop I have is people telling me.
AGPL v3. Self-host freely. Modify freely. If you run a modified version of HomeKeep as a public service, the AGPL asks that you publish your modifications so users of that service can see what's running. Same spirit as the rest of the project: transparent, self-hostable, yours to change — just keep the changes visible to the people you serve.
Found a vulnerability? See SECURITY.md for our threat model,
disclosure policy, and response SLA. Operators running HomeKeep on a
public domain should also work through
docs/deployment-hardening.md before
cut-over.
Every HomeKeep build ships a small, static, zero-telemetry JSON probe at
/.well-known/homekeep.json. It returns the app name, source repo URL,
license, and the unique build UUID baked into that image at Docker build
time. No phone-home, no analytics — the endpoint only responds to a request
made directly to the serving host.
curl -fsS https://your-homekeep.example.com/.well-known/homekeep.json
# => {"app":"HomeKeep","repo":"https://github.com/the-kizz/homekeep","license":"AGPL-3.0-or-later","build":"hk-<uuid>"}If you ever come across HomeKeep running on a domain that isn't yours or
mine, that endpoint is the fastest way to see where it was built and to
verify the AGPL license declaration. A build value of hk-dev-local means
someone rebuilt the image without passing the HK_BUILD_ID build-arg — a
signal (not proof) that the deploy isn't from an official release.
- Written during a few long evenings with Claude Code driving the build (the whole thing was scaffolded, researched, planned, and implemented via GSD — see
.planning/for the phase-by-phase paper trail). - Inspired by every task app that nagged me about annual gutter cleaning in July.
- Thanks to PocketBase, ntfy, shadcn/ui, and s6-overlay for doing the heavy lifting.







