Why I Built My Own 'Second Brain' Instead of a Notebook (and What It Taught Me About Data)
Not an app for other people — a tool built purely so that work spread across dozens of projects and two machines wouldn't forget what had already been done, and why.
The problem the app solves
When you're managing dozens of projects at once — homelab services, custom apps, client work, across multiple machines — a specific kind of problem shows up: context gets lost. You come back to a project a week later and have no idea what exactly was being done then, why something was done exactly that way and not another, and what's still waiting to be finished. The usual fix — "keep notes in .md files as you go" — sounds reasonable, but in practice has one weakness: it's an extra step with no mechanical connection to the actual work. It holds up for a short task. Over a longer, in-progress stretch of work, it gradually falls away.
That's exactly what an audit confirmed — for the kind of entries that were supposed to be logged continuously, as many as 10 of 16 expected entries for a single day were missing. The rule existed, it was even clear, but nothing enforced it — so it simply got skipped.
First attempt: write to two places at once
The first reaction was logical — if the .md write keeps getting skipped, add a second, redundant place to write to (dual-write): an app with an API that also keeps its own database. Every write should go to both targets at once.
The problem is that "write to two places" has exactly the same weakness as the original rule — it's still an extra step with no mechanical enforcement. It worked better for a while, but in the end it was the same kind of fragility, just with one more place to write to.
The decision: one write path, one source of truth
The eventual fix wasn't "be more disciplined about writing" — it was changing what counts as the source of truth in the first place. Instead of two equally-valid write targets, there's now exactly one — an app with an API — and the original .md files turned into a purely generated, readable backup, refreshed automatically once an hour by a script running in the opposite direction from the original importer.
The consequence is simple but important: there's now exactly one place where a write can go missing — and when it does, it's trivial to check (the last write timestamp, via a dedicated sync-status endpoint), instead of arguing about whether it's maybe just sitting in that other file.
The road there wasn't straightforward — data bugs found along the way
The move to a single source of truth uncovered a string of real bugs that simply hadn't been visible before, because the data lived in more than one place at once:
- Case-sensitive project duplication. The write code's own lookup for creating a new project handled case differently from the later lookup used for searching — the result was two separate projects,
Mimirandmimir, meant to hold the same thing. The exact same class of bug later turned up a second time, in a different part of the code (a live write from a parallel run on another machine), confirming it wasn't a one-off typo but a pattern — anywhere a lookup-or-create happens by text name, case has to be handled explicitly. - SQLite schema ordering. Adding a new column to an existing table crashed the backend right at startup with "no such column" — even though the command to add it was right there in the code. Cause: SQLite executes an entire block of statements in the order they're written, and a
CREATE INDEXon the new column tried to run before a separate function had finished runningALTER TABLE ADD COLUMN. In a system without a proper migration framework, the order has to be explicit: unchanged schema first, then an idempotent backfill of new columns, and only after that anything that depends on those columns. - API responses that were too large. The project list returned the full notes text for every project on every call — for some projects, over 70 KB. That was fine for the app's own normal reads, but reading it via the API exceeded a sane limit for a single fetch and ended up less convenient than just opening one
.mdfile. Fix: a lightweight list endpoint (just a "has notes" flag, not the content), with the full text only fetched on request for a specific record. The same principle was later applied to the entries list too (an optional trim of the text to a few hundred characters).
The big cleanup: when a duplicate doesn't look like a duplicate at all
The most interesting part came from hunting down duplicate entries created because the same event got logged twice during the transition period — once via the live write, once via a backfill import from the .md backup of that same period.
The first, simplest approach — an exact match on date and title — only found part of them, because the wording between the two writes of the same event differed slightly (punctuation, diacritics). Widening it to a "fuzzy" comparison of normalized titles found more pairs, but still not all of them.
The real fix only came from comparing content, not just the title — Jaccard similarity (word overlap) between the bodies of every entry within the same day, across the entire dataset, not just the part where duplicates had been reported. This approach found substantially more real duplicates, but it also brought an important trap that had to be avoided: in one particularly dense saga (several attempts in a row to fix the same bug), neighboring but genuinely different entries ("test succeeded" vs. "test initially mislabeled as solved — correction") had text similar enough that a cruder approach would have wrongly merged them as duplicates. Only a sufficiently high similarity threshold — combined with actually understanding the content, not just surface matching — could reliably tell apart "this is the same thing written twice" from "this is a claim and its later correction."
Altogether, 46 duplicate entries were found and cleaned up this way across several rounds — exact title match, fuzzy title match, content similarity, and finally one fully identical pair that turned out to be an artifact of the original parsing method for the old files.
What's left as a lesson
- A rule with no mechanical connection to the work fades away over a long enough stretch — no matter how reasonable it is. Dual-write is no exception; it has exactly the same weakness as the original manual rule.
- A single source of truth is easier to verify than two synchronized ones. Instead of "let's hope they both agree," it's one question: did it get written where it's supposed to?
- Splitting data across multiple places hides bugs. The case duplication, the schema ordering issue, and the duplicate entries were all present before — they only surfaced once the data had to live in one place and be internally consistent.
- Looking for duplicates by surface (title) alone isn't enough. Actual content is a more reliable signal, but the threshold has to be set carefully, so "the same thing twice" doesn't get confused with "similar, but deliberately different."
An app that started out just so I wouldn't have to remember context across dozens of projects in progress ended up becoming a small exercise in data integrity too — exactly to the degree a personal tool like this deserves: solid enough that what's in there can actually be trusted to be true.