← All posts

Why I built Projelli on Markdown instead of a database.

By Jameson Daines · 2026-04-27 · 9 min read

The first real architecture decision I made for Projelli, sometime in week 1, was: where do AI conversations live on disk? The default answer for an app like this is "in a database, probably SQLite." That's what most note-taking apps do. That's what Obsidian's chat plugins do. That's what Reflect, Mem.ai, and Tana do (in their cases, in the cloud rather than locally, but the data model is the same).

I went with the other option. Every Projelli AI conversation is a single Markdown file in a folder you pick. Eight weeks in, I'm convinced this was the right call for reasons that took me a while to articulate. Worth writing down because the trade-offs aren't obvious until you've lived with both.

The conventional architecture, briefly

Most chat apps store conversations in a database. SQLite for local, Postgres for cloud. The schema looks roughly like:

CREATE TABLE conversations (
  id TEXT PRIMARY KEY,
  title TEXT,
  model TEXT,
  created_at INTEGER,
  updated_at INTEGER
);

CREATE TABLE messages (
  id TEXT PRIMARY KEY,
  conversation_id TEXT REFERENCES conversations(id),
  role TEXT,
  content TEXT,
  tokens INTEGER,
  created_at INTEGER
);

This is what ChatGPT, Claude.ai, Notion AI, and basically every "modern" chat app has under the hood. It's the right shape for the things you'd naturally want to do: query messages by date, count tokens, build a chat history view, export to JSON.

It's also the wrong shape for what I actually wanted Projelli to be.

The thing I wanted Projelli to be

I wanted a tool where, two years from now, an indie founder could grep across every strategic conversation they'd had with the AI and find the one thing they'd written about a pricing experiment. They could open it in Vim if Projelli stopped existing. They could put the whole thing in a git repo and version-control their evolving thinking.

None of those work with a SQLite chat database. The data is technically yours. Practically, you need a tool that knows how to read the schema, and the schema is mine to change.

The Markdown-files approach inverts every one of those:

The deeper version of this argument is in the cornerstone page at /markdown-for-ai. This post is the engineering version: what it's actually like to build with this constraint.

What changed in the architecture

The Markdown-first decision flowed through to a lot of secondary choices. The biggest:

1. The "current state" of the app is the file system

Most apps have an authoritative in-memory state that's serialized to disk for persistence. The pattern is: load on startup, modify in memory, save changes. The disk format is a serialization of the in-memory model.

Projelli inverts this. The file system is the source of truth. The in-memory representation is a cached projection. When you change a file from outside Projelli (in your text editor, via a script, by syncing from Dropbox), Projelli notices and updates its cached view. The disk format is human-first; the in-memory model is the secondary.

Concretely: .aichat files are Markdown with a YAML frontmatter. The Markdown body is the conversation. The frontmatter has the provider, model, timestamps, tags. When you open Projelli, it scans the workspace folder, parses each file's frontmatter, builds the chat list. Edit a file in TextEdit, save, the chat list updates.

2. There's no SQL

I was prepared to add SQLite for full-text search across the workspace. Turned out I didn't need to. MiniSearch handles full-text search across thousands of Markdown files in milliseconds with a tiny index footprint. The whole search subsystem is one file.

This is partly because the data volumes for an individual workspace are small (a few thousand files at most for any reasonable founder archive). At a scale of millions of files, you'd need a real index. At the scale of one founder's strategic archive, you don't.

3. Sync becomes the user's problem in a good way

I'd been planning to ship a Projelli sync feature in v2. Eight weeks of using the Markdown-files approach later, I'm not going to. Users put the workspace folder in Dropbox or iCloud and sync just works. It's also more reliable than any sync I'd build myself, because Dropbox and iCloud are dedicated sync companies with hundreds of engineering years invested in the problem.

"Use the user's existing sync layer" is a real architecture pattern. Most apps reflexively try to own sync because it sounds like a feature. For local-first single-user tools, BYO sync is genuinely better.

4. The "export" feature doesn't exist

One of the awkward parts of building a chat app on a database is that you have to ship an export feature. Users want their data. The export is usually JSON, which is tool-readable but not user-readable. Inevitably someone asks for Markdown export of their JSON dump.

With Markdown-first, there's no export. The data is already in the format you'd export to. Users can cp -r their workspace folder anywhere. They can email it to themselves. They can put it on a USB stick. Built-in.

The trade-offs I accepted

The Markdown-files approach isn't free. There are real costs:

1. Some queries are slower

"Show me every conversation where I used GPT-4 between March and April with more than 5,000 tokens" is a SQL query. With Markdown files, it's a script that walks the workspace, parses frontmatter, applies the filter. The walk is fast (MiniSearch's index is rebuilt on workspace open) but for ad-hoc filtered queries, you don't have the SQL primitive available.

For an indie founder workspace this rarely matters. For a multi-tenant cloud product where you'd run analytics queries across millions of conversations, it would matter a lot. Different shapes for different scales.

2. Concurrent writes are tricky

If two processes write to the same file at the same time, you can lose data. With a database transaction, you don't. Projelli is single-user single-process so this is rarely an issue, but if a user runs two Projelli instances pointed at the same workspace folder (which Dropbox-synced setups can produce), you can hit conflicts.

The mitigation: every file write goes through a debounce + atomic-rename pattern. Conflicts are still possible but rare. For a multi-user collaborative app, this approach would not scale; for single-user, it's fine.

3. The frontmatter parsing is its own subsystem

Reading a YAML frontmatter from each file on workspace open is more work than a SQL SELECT. The cold-start indexing of a 1,000-file workspace takes ~200ms. Once indexed in memory, queries are fast, but the first open is slower than a SQLite-backed alternative.

Acceptable for a desktop app where workspace open is a once-per-session event. Not acceptable if you needed to start fresh on every API request, which a cloud app would.

The cultural side of the decision

The architecture decision was also a cultural one. By choosing Markdown files, I committed Projelli to a specific worldview:

This is the part that's hard to feel until you've used a tool that does the opposite. If you've ever paid for a notes app for years and then watched it sunset and discovered the export feature was JSON, you know the cost of vendor lock-in by data format. The Markdown-first decision is the architectural defense against that pain.

What I'd do differently if I were starting over

Honestly, not much. The Markdown-files approach has been more flexible than I expected. The two things I'd consider:

1. Start with the format design, not the editor

I designed Projelli's .aichat file format somewhat opportunistically. Frontmatter, then alternating ## You / ## Claude sections. It works, but I'd version the format spec from day one and write a formal document about how it should be parsed by other tools. Right now the spec lives in code; that's fine for me but suboptimal for other tool builders who might want to read or write the format.

2. Ship a CLI from day one

Markdown-as-data should come with a CLI. projelli list, projelli search "pricing", projelli export-as-pdf. I haven't built one yet because the GUI handles 95% of use cases. But CLI access is what makes the "your data is yours" claim more than rhetoric, and shipping it from day one would have been the right move.

The takeaway for other builders

If you're building a personal-data tool (notes, AI workspace, journal, knowledge base) and you're considering whether to use a database or files, here's the test: do you want your users to be able to read their data 20 years from now without your software?

If yes, files. The trade-offs of databases (SQL queries, strict schemas, transactional safety) don't outweigh the loss of long-term data ownership for a personal tool.

If no (because you're building a multi-user collaborative app, or your data model is genuinely too complex for files, or you're building for a context where SQL queries matter), databases. There are good reasons for them.

For Projelli specifically, the answer was clearly "files." Eight weeks of building on that foundation has been smoother than I expected, and the marketing claim ("your data is yours") is structurally true rather than aspirationally true. That distinction matters more than I realized at the time.

Try a Markdown-first AI workspace, free download