Build log
What it took to build this.
I build Vectrium on my own and it runs in production, so the mistakes are mine and the recoveries are documented. These are the decisions that shaped it, including the ones that broke something first.
Abandoning the fork
I started from a fork of an existing chat app. Six weeks in, I had modified 116 upstream files despite a rule that I would only add, never edit, and every new idea meant digging through 100,000 lines of someone else’s code first. The real problem was that I was making chat extensible when what I wanted was for chat to be one mode among several. I threw it out and started again with rooms and activities as the foundation.
Rust on the backend, deliberately
Realtime collaboration means several writers hitting shared state at once, which is where dynamic languages happily let you ship a race condition. I picked axum and sqlx so that queries are checked against the live schema when the code compiles. It made prototyping slower, and I still think it was right: a wrong column name or a malformed event now fails the build instead of surfacing as a corrupted row three weeks later.
The WAL lock that taught me deploy discipline
I hot-swapped a container by hand with docker rm -f. The SIGKILL left the SQLite write-ahead log on a network mount in a state the new container could read but not write, so every write failed for seven minutes while I worked out why a healthy-looking deploy was broken. The fix was procedure, not code: a deploy script that stops gracefully, checkpoints the log, and waits on a health check that takes a real write lock — so green means the database accepts writes, not just that a port is open.
CI/CD, because two agents can clobber each other
Deploys were manual — clone, rsync, build, run. That works alone and falls apart with several agents working at once: one agent’s rsync --delete could wipe another’s half-finished build, and an old clone could quietly undo work already merged. Moving to GitLab CI made origin/main the only place changes come together, and everything after that point is the same every time. If a new image fails its health checks within 60 seconds the previous one comes back automatically, and CI still reports the failure so nobody thinks it worked.
Drafts, mentions, and the mobile surface
This was the stretch of filling in what a surface needs to actually be usable every day: mentions that survive a round trip as real references rather than text, a drafting pane for longer writing, read tracking per person, and push notifications. I built the phone experience properly instead of shrinking the desktop one — the room shell, the action sheets, and both editors are built to work on a phone, because that’s where I read things when I’m not at my desk.
SQLite to Postgres
SQLite was the right call for the first two months and the wrong one by the third. Several agents writing at once, against a data directory on a network mount, is not what it’s built for. I moved to Postgres in stages — port the schema, write to both and compare, then cut over — and added a check in CI that rejects the two specific migration patterns that had already cost me data twice.
Context bands for very long sessions
Agent sessions produce enormous transcripts, and what you want from them later is almost always what was decided rather than what was said. Long conversations now group themselves into chapters with summaries, plus a bar that tracks where you are and an outline on desktop. A working session of a thousand messages stays something you can navigate instead of an archive you scroll past.
