How Mind Archive is built

React, FastAPI, SQLite and a folder of Markdown. A short tour of the architecture, and the two constraints that decided most of it.

Mind Archive is a small application, and staying small is a design goal rather than an accident. Here is the shape of it.

Web UI  (React + TypeScript + Vite)
   |
API     (Python + FastAPI)
   |
   +-- Importers   one adapter per provider
   +-- Archive     reads and writes the files
   +-- Search      full-text, over SQLite FTS5
   +-- Events      an in-process bus
   |
Local filesystem + SQLite

Two constraints decided almost everything else.

Constraint one: the files are the truth

User content is Markdown, JSON and plain text on disk. SQLite holds metadata and search indexes only, and must be rebuildable from the files at any time.

This sounds like a storage detail. It is actually the product. It means:

The moment the database becomes the source of truth, all four of those go away. So it does not get to be.

Constraint two: no provider is special

Every AI provider lives behind an importer adapter, and core code never imports a provider-specific module. Adding Gemini means adding one adapter and one test; it does not mean touching the archive, the search, or the ChatGPT importer.

This paid off faster than expected. ChatGPT and Claude both export a file called conversations.json, and the original importer matched on that filename — so it would have confidently claimed a Claude export and reported it empty. Detection now inspects the shape of the file instead. It was a latent bug that no amount of testing one importer could have found, and the fix stayed inside the adapter layer where it belonged.

The interface is one page

No router. No sidebar. No dashboard.

There is one view — your archive — with search, import and status on it. A conversation opens in place rather than navigating somewhere. When the page grew long enough to need it, we added in-page anchors in the header, which is navigation within one view rather than between several.

It is styled with plain CSS and custom properties. No component framework, because a framework is a large dependency that pulls an interface toward looking like every other interface built with it. There are three palettes and a Light / Dark / System theme, all driven by two attributes on <html> and one set of tokens.

Events, without the infrastructure

There is an in-process event bus — about forty lines. archive.imported fires, things listen.

There is no message broker, no queue, no Redis. For a single-user application running on one machine, those would be operational weight bought against a requirement nobody has. The extension point is real; the infrastructure behind it is a function call.

Docker is the supported path

Not because containers are fashionable, but because of a specific problem: the machine this project started on had Python 3.7 on Windows and 3.8 in WSL2, both end-of-life and neither able to run a modern FastAPI stack. That is a common situation, and Docker removes it as a question:

python scripts/dev.py up --build

Native setup is documented and fully supported for anyone who prefers it.

The stack, and why each piece

Piece Why
React + TypeScript + Vite Types catch the class of bug that ruins a data tool; Vite is fast and unopinionated
Python + FastAPI Typed request models, and the ecosystem for reading messy archive formats
SQLite + FTS5 Real full-text search in a file. No service to run, no port to secure
Plain CSS No framework, no generated look
Docker Compose One command, identical everywhere

Nothing here is exotic, and that is the point. The interesting decisions are about what was left out — which is its own post.