Habit Jr. is a routine app for kids on iPhone and iPad, with an API behind it. I did the product design, the app, the API, the servers and the monitoring myself. This page covers how it went together.
Parents set up routines for their kids, ages 4 to 12. Kids check off tasks on their own device and earn points and stickers. Parents get a weekly summary.
Because it’s for children, a few rules came first: no ads, no photos, no free text from kids, and no rankings between siblings. It also has to work offline, since a kid’s tablet loses Wi-Fi.
The design process was light. I wrote a spec and made an HTML mock of the screens. When the two disagreed, I picked one, wrote the reason in a decision log, and moved on. I went native Swift over React Native, and I cut the task screen down to one menu because two buttons on every row was confusing.
Parents sign in with Apple. Kids don’t have accounts. A parent creates a child profile and links the child’s device with a QR code, and that device can only ever see that one child’s plan. The worker creates upcoming tasks 35 days ahead and closes out each day and week.
Hono on Node, Better Auth, PostgreSQL and zod.
A family’s data should never show up in another family’s request, so I made the database enforce it. Every table has a row-level security policy, and the API connects as a role that can’t turn it off. Each request starts by telling Postgres which family it’s acting for. Here’s the policy for one table:
ALTER TABLE app.occurrences ENABLE ROW LEVEL SECURITY;
ALTER TABLE app.occurrences FORCE ROW LEVEL SECURITY;
CREATE POLICY household_scope ON app.occurrences
USING (household_id::text = current_setting('app.household_id', true))
WITH CHECK (household_id::text = current_setting('app.household_id', true));
-- per request, inside the transaction:
SELECT set_config('app.household_id', $1, true);A few other decisions shaped the rest of it:
The integration tests run against a real PostgreSQL as the restricted login. They cover cross-family access, adult roles, child credentials, revoked links, and duplicate or stale requests.
About 11,000 lines of SwiftUI for iPhone and iPad.
The app handles screens, caching and queueing. It doesn’t calculate scores or generate recurring tasks, because the server does that.
The first build has everything except push notifications. A TestFlight build is uploaded. Testing on real devices, legal review and a security review still have to happen before release.
Small and repeatable.
On every pull request, CI type-checks, checks that the OpenAPI file is current, runs the domain tests, applies every migration to a fresh PostgreSQL and runs the integration suites. A macOS job builds the Swift contract code and tests the encrypted local store.
A merge to main builds one container image tagged with the commit SHA. A self-hosted runner deploys it to staging with Helm over a private network. A one-off Job applies migrations as the database owner, then the API and worker start with a restricted login.
Production runs an image that has already run on staging, picked by tag and never rebuilt. A rollback means deploying the previous tag. Every migration so far only adds things, so the previous image keeps working.
| Target | What it is | State |
|---|---|---|
| Staging | One small server running k3s, with its own PostgreSQL | Live, behind a Cloudflare tunnel with no open ports |
| Production | A second server joined to the same cluster, plus a managed PostgreSQL, defined in Terraform | Not launched. About $27 a month |
| iOS | TestFlight builds from my Mac, started by a GitHub Actions workflow | Build 0.1.0 uploaded |
To save the cost of a second cluster, production joins the staging cluster as a worker node. If the staging server goes down, production keeps serving but can’t be restarted or redeployed until it’s back. I’ll revisit that once there’s real traffic.
Backups are a nightly encrypted pg_dump to a private bucket, kept for 30 days. I haven’t done a full restore rehearsal yet.
Kids’ data limits what I can log, so most of this is built from IDs and events.
There’s no metrics or alerting yet, and the health endpoint doesn’t check the database.
Tell me what you have in mind and I’ll tell you how I’d build it.
Habit Jr. is my own product. Figures come from its repository as of October 2026 and will change as it nears launch.