Logging one-handed at 3am
Most apps are designed for someone sitting up, with both hands, in reasonable light, on a network. A baby logbook gets used by someone lying sideways in the dark at 3am, holding a sleeping newborn on one arm, with the other arm pinned, who will not remember any of this tomorrow and who is one loud noise away from restarting the last hour.
That is not an edge case to handle gracefully. It is the primary case, and it turns out to decide almost every technical choice in the product.
One thumb, and only one
Everything you do often is one tap, and every one of those taps is inside the arc a thumb can reach without the hand shifting grip. That mostly means the bottom third of the screen, and it means primary actions never live in a top-right corner, where a design tool would happily put them.
Touch targets are 56pt for anything you press half asleep — the platform floor of 44 is a floor, not a target. There is no size below that in the design system at all, because the moment one exists somebody uses it for a destructive action.
The one that surprised me: the app tells you which breast to start on. It is a single line on the home screen and it is the most-thanked feature in the product, because at 3am nobody remembers, and getting it wrong is a real annoyance for both parties. Nothing about that is clever engineering. It is just noticing that the question exists.
The dark is a design constraint, not a theme
Dark mode here is not a preference to be honoured somewhere in settings, it is the default operating environment. Which means: no white flash on launch — ever, and this is harder than it sounds when a splash screen hands over to a JavaScript bundle. No confirmation that flares brightly. No animation with personality, because you will see it for the hundredth time tonight and it will be in your way.
The opening animation is 1.1 seconds, one movement, no bounce. When the system asks for reduced motion, it does not run at all.
Every write is local, and nothing waits for a server
The write path is the same for every kind of entry, and it is the spine of the whole app:
write to SQLite → mark the row dirty → nudge the sync loop → return
The nudge is not awaited. Nothing in the interface ever waits for a network round trip, which is what makes the app behave identically in a lift, on a plane, in a hospital basement, and in a nursery at the back of a Dutch house with thick walls. The sync loop pushes dirty rows whenever it can; if that is in six hours, the log was still complete six hours ago.
This is also, quietly, the reason the whole logbook is free forever. It costs us almost nothing to run — a row of text that syncs eventually is not an expensive product — and charging for something that costs nothing is a bad deal dressed up as a business model.
Three things about that path were harder than expected.
updatedAt is a logical clock, not a timestamp. Two phones write the same record; the later write should win. Wall-clock time cannot decide that, because a phone with a fast clock will silently discard every correction made on the other device after it pulls from it. So the counter increments, and — the part that is easy to miss — it also observes remote values and jumps ahead of them.
Last-write-wins ties break on a content fingerprint. Two writes at the same logical time need a deterministic winner, and both client and server must pick the same one. The obvious tiebreak is the row id, which is inert: it is the same row on both sides. Hashing the content works, and it converges.
The sync cursor never advances past a row the client could not parse. A pull asks for everything with a sequence number above the cursor. If a client skips a row it did not understand — say, an entry kind added by a newer version of the app — that row is gone for that device forever. So the cursor stops below it and waits for a version that can read it.
The bug that held the app on its splash screen
My favourite failure of the project, because it passed every automated check.
The database provider renders nothing until the SQLite file is open — a few milliseconds, invisible behind the splash. The splash overlay sat below that provider in the tree, and took a ready prop from above it.
When the provider finally rendered its subtree, it rendered the children it had captured before the wait. ready had flipped to true in the meantime. The overlay received the stale value, once, and never got an update. The app sat behind its own splash screen forever, with no error anywhere: the JavaScript was running, the database was open, the router was mounted, and the screen showed a logo.
Contexts pierce that gate fine. Props freeze. The fix was to move the splash above the store — but the lesson was that the failure looked exactly like a hang, and hangs are the hardest bugs to reproduce from a report that says “it doesn’t open”.
Two platform landmines, for the search engines
Never do date arithmetic by adding 86,400,000. A day is 23 or 25 hours twice a year, and a baby logbook is made of day boundaries. The tests are pinned to TZ=Europe/Amsterdam so that the two broken days a year are always in the suite.
Intl.NumberFormat.prototype.formatToParts crashes on iOS Hermes. Not throws — crashes. DateTimeFormat.formatToParts is fine, which makes it a delightful thing to debug.
What we deliberately did not build
No streaks. No points. No badges, no targets, no weekly summary telling you how you did. Nobody needs a medal for feeding their own child, and a streak counter turns a record you keep for yourself into an obligation that can be broken — at exactly the moment your life is least able to absorb another obligation.
There is a collection, because a keepsake is a nice thing to have: first bath, first outing, small moments worth photographing. But it is a collection, not a score. Nothing is ever empty-with-a-gap-where-an-achievement-should-be, and the count is simply absent until there is something in it.
And, most importantly: the app never tells you whether anything is normal. It states what you wrote and refuses to interpret it — a refusal that is enforced by a test that reads every string in three languages and fails the build when it finds the vocabulary of reassurance.
The thing I would tell anyone building for a hard moment
Design for the worst version of the moment, not the demo version. The demo is a parent at a kitchen table with two hands and good light. The real one is dark, one-handed, exhausted, and probably offline — and if the app works there, the kitchen table takes care of itself.
Hodierna is a baby logbook that works offline and is free forever, in Dutch, English and Turkish. Built by a parent, during those exact nights, for his own daughter.