Sprint 38 — a key of your own


At the end of last sprint I said this was the one I was most looking forward to: per-household encryption, and the freedom to choose your model provider. The first shipped and is the largest single feature I’ve built for Domi. The second I built — and then took back out, for a good reason I’ll get to. Ninety-one PRs, plus the kind of depth and polish that only shows up once you’ve been living in the app for a few weeks.

A key of your own

Here’s the thing a household app has to earn before anyone else’s data goes in it: a credible answer to “what happens if someone steals the database?”

Domi now has one. When you turn on encryption at rest, your household gets its own encryption key, and every piece of sensitive data — the text of your notes and tasks, your contacts and their phone numbers, your members’ dates of birth, your transactions and the amounts on them, your chat history, your document filenames and the original files themselves, even the search index built from them — is sealed under it. If our database host were ever breached, or our file-storage provider were served a court order, what they’d hand over is unreadable bytes. The key that unlocks them isn’t stored there.

I want to be precise about what this is, because security writing is usually dishonest by omission. This is encryption at rest, operator-assisted — not zero-knowledge. Your household key is itself wrapped by a master key that lives inside the application, separate from the database and the file store. That’s a deliberate trade: because the app still holds the master key, search and reminders and the assistant and the nightly automation all keep working exactly as before, and there’s no vault password you can lose and lock yourself out forever. What it protects you against is a stolen database or a stolen backup — a real and common failure mode — not Domi itself. I’d rather say that plainly than imply more than the design delivers.

The master key lives in the hosting platform’s encrypted secrets store, separate from the database and the file storage. Access to it inside the app is limited to one restricted place. Today, realistically, that’s one person — me. (Corrected 2026-07-13: an earlier version of this post said the key was also held in custody with a notary — that arrangement was planned but hasn’t been put in place, so the claim is removed until it is.)

The whole thing is off by default and opt-in per household, activated from Settings by an owner. It converts your existing data in place, with no downtime and nothing to re-enter, and it’s safe to resume if it’s ever interrupted. The algorithm, for the curious: 256-bit XSalsa20-Poly1305, the well-audited libsodium “secretbox” construction — which, being symmetric and 256-bit, stays out of reach of even a future quantum computer.

I built it across the entire data surface, then activated it on a real staging household end to end and watched it convert fields, files, and the search index without dropping a thing. That last part — proving it on a real plane, not just in tests — is what moved it from “written” to “shipped.”

The wrong way to pick a brain

The second promise was “choose your AI provider,” and I want to be honest about how it went, because it’s the second time this month I built something and then deleted it.

I shipped it: a setting where a household picks Anthropic, OpenAI, or Cohere, and every AI task follows that choice. It worked. And then I looked at it and realized it was the wrong shape.

The whole point of the model-routing layer I built in the very first sprints is that the app asks for a task — “classify this document,” “plan these steps,” “read this scan” — and the system picks the right model for that task. Different tasks genuinely want different models: reading a scanned PDF needs vision, which Cohere’s text model doesn’t have, so picking “Cohere” for your household just meant document-reading silently fell back to something else anyway. A single per-household provider switch took that careful per-task routing and flattened it into one blunt lever — and worse, it turned model quality into a setting the user has to think about, when it should be an engineering decision I’m accountable for.

So I removed it (it’s written up as ADR-0007, if you like that sort of thing). The routing layer stays — it’s exactly what makes this possible — but it routes per task, not per household. Each role is mapped to the best model for it: the conversational assistant and the planner on Anthropic’s strongest models, the cheap high-volume classification on a small fast one, embeddings and re-ranking on Cohere, document extraction decided by a head-to-head eval. And every one of those choices is gated by an eval before it flips. You don’t pick the brain. Picking the right brain for each job is my problem, and it should be.

The early architecture bet still paid off, just not the way the last post predicted: the gateway didn’t make “pick your provider” cheap — it made not needing to a coherent decision instead of a limitation.

Documents that answer the hard questions

Domi could already answer “what did the vet say?” by searching the text of your documents. This sprint it got materially smarter at the hard questions — the ones where the answer is buried in a table on page nine.

The retrieval got three upgrades at once: search results can now be filtered to a specific area or asset, the assistant can pull up a parent document when a snippet isn’t enough, and a re-ranking pass plus a hybrid keyword/semantic search means the right passage actually surfaces. And I found and fixed a quietly embarrassing bug: brokerage statements were being classified as “transactional,” which meant Domi skipped the full transcription — so the holdings and returns tables never made it into the index at all. “What’s in my portfolio?” was unanswerable not because retrieval was weak but because the data never arrived. It does now.

Recurring, properly

Recurring events are one of those features that look done long before they are. This sprint they got the parts that actually make them livable: end conditions (stop after N times, or on a date), proper projection of future occurrences onto the calendar, and — the one everybody eventually needs — editing a series as this-and-all-following versus the whole thing. The difference between “I have a calendar” and “I trust my calendar.”

The evening it didn’t work

Every sprint has one of these, and honesty about them is the point of writing these at all.

With encryption built, I went to activate it on staging — and it failed: a storage key “not set.” Except it was set; I could see it in the dashboard. What followed was a properly annoying hour. First I diffed every environment variable between staging and production: the key was there, configured identically to the three siblings next to it that worked fine. Then the runtime logs gave it away — the failing activation was running on an old deployment. I’d redeployed, but the staging domain was still pointed at the previous build; the alias hadn’t moved. And underneath that, the actual value of that one key really had been saved blank.

Two unrelated gremlins stacked on top of each other to produce one misleading error. The fix was thirty seconds; finding it was the evening. I wrote both down so the next person — almost certainly future me — loses thirty seconds instead of an evening.

The unglamorous half

The rest is the long tail that’s most of any real sprint: an upgrade to the latest AI SDK, a sweep of dependency updates, a pinned toolchain, and — small but it bugged me — release notes that no longer dump GitHub issue numbers at the end of every line. If you’re reading the changelog you want to know what changed, not which ticket it closed.

Next

Sprint 39 turns outward. The encryption work was the last big inward-facing thing; now it’s about Domi reaching into the places your life actually lives.

That means continuing to test in earnest — living in the app, finding the rough edges, fixing them — and making the assistant better at acting, not just answering. But the headline is connectors: deepening the Google email and calendar sync that already exists, then adding Apple iCloud mail and calendar, and — if the consumer tier cooperates — Microsoft 365 as well. A household-management app is only as useful as the household data it can see, and most of that data is sitting in an inbox and a calendar I don’t yet read well enough.

Encryption was the work that made beta safe. Connectors are the work that makes it worth it.