Project 02 06
Nido
A self-hosted property-tracking workspace built around the idea that losing data is worse than being briefly unavailable.
Nido is a self-hosted workspace for tracking properties — their fields, tags, engagement, and the analytics that come out of them — behind a single authenticated application. It is a monorepo: a Go backend with SQLite as the system of record, a React 19 and Vite frontend, local development helpers, documentation, and configuration all living in one place.
What it does
The workspace supports admin and operator roles over the same dataset. Properties carry custom fields and tags rather than a fixed schema, engagement data accumulates against them, and a reporting layer turns all of it into operational insight through its own APIs. The frontend never talks to raw endpoints; it goes through typed service modules that act as the contract between the Go handlers and the TypeScript components.
Data safety as a feature
The thing I care most about in Nido is its migration system. Most applications treat schema changes as an implementation detail that happens quietly at boot. Nido makes them explicit and reversible:
safe-autobacks the SQLite file up before applying any schema changemanualrefuses to migrate at all until someone decides to- exports are portable JSON, not a binary dump tied to one version
- restore workflows and migration status checks are first-class commands
The runtime deliberately prioritises data safety over availability. If a migration looks risky, the correct behaviour is to stop and wait for a human, not to start and hope.
What I learned
Building Nido taught me that the boring infrastructure is the product. Anyone can add a table; the question that matters is what happens the one time a migration goes wrong at 2am on someone else’s server. Once I started treating backups, exports, and recovery as features with their own interfaces rather than as scripts in a folder, the whole design got simpler.
Typed service modules were the other lesson. Go and TypeScript don’t share a type system, so the boundary between them is where drift creeps in. Funnelling every call through a narrow, explicitly typed layer meant a backend change surfaced as a compile error in the frontend instead of as a runtime surprise, and it made the API shape something I had to think about deliberately rather than accumulate by accident.
Running the whole thing as a monorepo with one validation pipeline — frontend tests, linting, type checking, production builds, Go tests — removed the class of problem where two halves of a project drift apart because only one of them is actually being checked.