Sprint 36 — making room for other people


For six weeks Domi has had exactly one user and one developer: me. Sprint 36 was about ending both — not by inviting anyone yet, but by building the machinery that makes inviting people safe. Thirty-nine PRs, almost none of them the kind a user would notice, nearly all of them the kind you need before a stranger touches your code or your data.

More than one user

The four household roles — owner, member, viewer, guest — have sat in the schema for weeks. Nothing enforced them. That’s a pattern I keep catching myself in: schema is not enforcement, and a permission that exists only as a column is a permission that doesn’t exist. So this sprint they got teeth — real server-side role checks.

Then the bigger piece: sharing groups. Every entity in the household — a document, an obligation, a contact — can now be scoped private, household, or to a custom group, the way 1Password lets you keep a personal vault next to the shared one. The rule that makes it trustworthy is that the owner doesn’t bypass it: if something’s private, it’s private in chat, in search, and in the knowledge graph too. The visibility boundary is the same boundary everywhere, not a UI suggestion.

And the email model got honest. The address you log in with isn’t the address you want to be notified at, isn’t the mailbox Domi reads for bills, isn’t the email printed on a contact card. Four different jobs that I’d quietly conflated into one field. They’re separated now, and a person can carry several emails. Boring to describe, load-bearing for everything multi-user.

More than one developer

The repository moved out of my personal GitHub account into an organization — Gailleur Labs. Both repositories, actually: Domi itself, and this site you’re reading.

Why now, and why bother? A second person is about to start on the Domi mobile app, and a third on the marketing side. A GitHub org isn’t a brand — nobody discovers a product by its repository URL — it’s where contributors, branch protection, and teams live. So main is now protected (every change lands through a reviewed PR), there’s a dev team and a marketing team, and the repos are named for what they are instead of for my surname. The studio stopped being a metaphor and became an org chart with two repos in it.

The migration itself was the careful, reversible kind of plumbing you only notice when it breaks: back up first, transfer, then re-point every local clone and reconnect both Vercel projects to the new owner. Invisible when it works; a 2 a.m. incident when it doesn’t. It worked.

A real production

I also wrote the plan to finally split the single environment Domi has run on since day one into proper staging and production. There’s one load-bearing decision in it, and it’s the opposite of the obvious one.

My family’s data — weeks of real documents, the predicted tasks, the actual graph of our household — becomes production, in place. It does not get migrated into a fresh empty prod, and it absolutely does not become “staging,” where regression suites and throwaway branches run. The irreplaceable thing stays exactly where it is; the disposable thing gets built new around it. When you’ve accumulated something you can’t recreate, you don’t move it to make room — you build the room around it.

Two architecture notes also landed: standardizing the whole model layer on one SDK across all thirteen workload roles, and a separate, privacy-gated look at routing through a gateway so I can run cheap cross-model quality comparisons later. Both written down as decisions, neither rushed into code.

The three-bug question

The humbling thread of the sprint. I uploaded a course-confirmation PDF for one of my kids and asked Domi what he’s taking next year. It couldn’t tell me. Getting to an answer took three separate “fixes,” each stacked on the last:

  1. The extractor had no field for a course list — it captured the header and silently dropped the ten courses it could plainly read off the page. Added the field. Fixed.
  2. Re-uploading the same PDF did nothing — deduplication correctly saw identical bytes and skipped re-extraction, and there was no way to force a re-run. Added a Re-extract button. Fixed.
  3. The action that attaches the courses to the kid’s school record matched only active enrollments — and his was still marked prospective, so it would have quietly created a duplicate. Fixed the match.

Each fix revealed the next one underneath it. It’s a lesson I keep relearning: “it works now” usually just means “the next layer is finally visible.” A bug is rarely alone.

Next

Sprint 37 is where the doors actually open. The environment split. WorkOS authentication — one identity for the web app and the API. And finishing membership, ending with the part that matters most: inviting people by email.

The goal is concrete and a little nerve-wracking: a real person who is not me, holding an account with real data, inside the dogfood — with the scaffolding under it solid enough that “beta” stops being a someday word. This sprint built the room. Next sprint, someone else walks in.