Forward
This is the second time I have written this post. The first time, after working for hours recalling and writing, the note I was writing was lost because of a bug in the Basic Memory Cloud web editor. Sometimes things are hard. I was determined to write it myself too. I did use AI to edit (my typing is terrible), and help dig out relevant commits and make sure the timeline is accurate.
I have jokingly called this piece "The Caveman Evolution of Basic Memory", because it captures some of the story from primitive, simple beginnings, and how it has evolved over time.
Finding some inspiration
When I first started working on what would become Basic Memory, the MCP spec had just been released. MCP (the Model Context Protocol) is the standard that lets an AI app call out to external tools — in practice, it's how you give an AI new abilities. I was using Claude Desktop and wanted to save info from my chats locally, not copy/paste back into the project knowledge all the time. The big pain I was feeling was starting over from zero with every new chat. Further, chats would suddenly just stop when you reached the context limit and you were SOL. This is still a pain, to be honest, but things have gotten a lot better, compaction works ok for the most part, AI-native memory remembers useful things most of the time. But way back then, in late 2024, things were pretty raw.
I had a coding project I was working on, a variation of shadcn UI components, but written with HTMX and Alpine.js. My idea was to make frontend development suck less for apps I wanted to build. I have always hated working in React. It makes zero sense to me to this day. What I discovered while implementing a bunch of these components is that AI could write them much better and faster than I could. I was literally copy/pasting code snippets in and out of the chat window and my IDE. To manage this, I had a bunch of Markdown notes in Obsidian and I was copy pasting back and forth.
I started using Claude Desktop when it came out, and it seemed to understand the gist of what I was trying to do very well. Soon thereafter, I saw the memory MCP (https://github.com/modelcontextprotocol/servers/tree/main/src/memory) and thought, I want "that". I could see that there might be some way I could use it to get out of my copy pasting long context over and over. So, I started doing what any developer would do, poking through the source code and stealing ideas. Instead of JSON though, I wanted Markdown, because I wanted to edit the files myself. That basic idea grew into what Basic Memory still is today, a bunch of Markdown files that get parsed and indexed into a "knowledge graph". I was already using Claude to write code, but still typing a bunch myself in the IDE. Then I started using the filesystem MCP and was like "holy crap, AI can do this faster than I can". So now I really wanted to make something that AI could really use to store memory so I could use it all the time.
Ch. 1 - Local-first, plain text, stdio, because that's what existed (Dec 2024)
From the git log:
- First commit f95a8562 (2024-12-02).
- First MCP server 18dd8796 (2024-12-08): low-level
mcp.server.Server+stdio_server(); tools reached straight into the DB viadeps.get_project_services(). No API layer.
Recovered artifact — basic-memory.md at the repo root had mermaid architecture sketches by 2024-12-05, three days after the first commit (view the file at that commit).
The Plan evolves
There were some ideas I had clarity about. I wanted Basic Memory to be based on plain text, but it took some experimentation and iteration to figure out how that would work. Day one already had the principle (cbf366a7, 2024-12-02) — a comment in the very first service code lays it out, "filesystem is source of truth":
- Write to filesystem first
- Update database indexes second
- Database is treated as disposable/rebuild-able index
But the flow was one-directional — the app wrote files as output (the Dec-05 sketch above even shows DB -->|generate| MD). The reverse direction — humans edit files anywhere (Obsidian, git, whatever) and the app detects the changes and parses them into the graph — landed a couple of weeks later with file_sync_service (a4c1989c, 12-19). Then I added proper markdown parsing of the file content (c9fec7ab, 8b26162a, 12-21). The Entity table grew a checksum column for change detection. By v0.1.0, basic-memory sync --watch ran a file watcher so edits made outside the app were picked up too.
Index for fast lookups
Now that the files were the canonical truth, the db index could be a derived artifact — something you can throw away and rebuild from the files in seconds. This principle ended up surviving every architecture that followed. Ten months later, deep in the cloud storage struggles, we would write the same sentence in a spec:
"The SQLite database is just an index cache… It can be rebuilt in seconds from the source markdown files."
The sync and indexing code itself would be rewritten several times over, local and cloud — but that's a later chapter.
The first MCP spec (2024-11-05) defined two transports: stdio and HTTP+SSE. Stdio was just the only one that mattered in practice (Claude Desktop only spoke stdio).
I had to read that spec (myself) at least ten times before I understood what a "server" or a "host" was and just where and how the runtime worked. Stdio is at once very powerful (hello Unix CLI toolset) and also very limiting (for example error handling). But being able to plug into an AI app (what we now call a harness) was super cool. Since all of this was new, I started looking at the Python SDK examples and was like, WTF? This is terrible. I built the first version with the low-level SDK anyway. Luckily, a couple of weeks in, I found FastMCP, which had just come out, and I started using it, since it had a similar usage flow to FastAPI, which I was already a big fan of.
Markdown format
Working with Claude, I came up with a basic structure to re-create the simple data model from the Memory MCP. Entity, Observation, Relation. Looking back now, this was a real missed opportunity to rename "Entity" to something better.
Entity
An entity is a node in the knowledge graph. This became a Markdown note, with frontmatter and text.
---
title: An Entity is a Markdown file
permalink: a-slug-for-the-entity
type: anything
tags: ["whatever", "you", "want", "here"]
---
# About an Entity
The rest is just plain text Markdown
Observation
An observation is a fact about an Entity. They always refer to exactly one Entity. I wanted something simple to record these. In Markdown, it's really easy to make lists, so I just added a special [category] marker to a regular Markdown list to record Observations with a category type.
- [fact] an Observation is a list item with a string in brackets at the beginning
Relation
A relation is a directed connection between two Entities: it has a type, a source, and a destination. The source is the Entity note which contains the Relation. The destination is the Entity referenced. Relations are [[wikilinks]]; they can also be written as a Markdown list item to declare the type.
- related [[Some other Entity]]
The value inside the wikilink is matched in a few ways — by exact string or by fuzzy text search — so links don't have to be exact.
Data model
The data model was deliberately simple too, just those three tables and some properties.
(This is the actual v0.1.0 schema. Note to_id is nullable and to_name keeps the raw [[wikilink]] text — an unresolved link just waits until a matching entity shows up.)
I added full text search and indexed the title and Markdown body. Even with just these things it was pretty clear that search, combined with a few hops through the knowledge graph was a pretty powerful combo. The first operation I implemented to load context for build_context was:
- search via full text search
- for each top result
- find the next related results
- include a small summary and id for each
With this, the AI could search iteratively with a few calls and find a really wide set of notes, then choose which ones looked most relevant. This is almost like "Graph RAG" without the RAG (there were no embeddings yet).
Prompting Patterns
The pattern I was figuring out was that the tools themselves don't have to be fancy, or "intelligent". The AI model, using simple tools, can decide how to call them. Doing small simple things lets the model be the star. The better the model can access the knowledge base, the more useful it is for the user.
This led to a few other patterns I'll point out.
Be helpful
When there were no results found, instead of returning a terse return code, the tools prompted the model to try other operations, or prompt the user for a possible next action. This keeps the AI from getting stuck or giving up too quickly. The tools were really an interface to prompt the AI more effectively. This is now called "context engineering", but it's very natural if you think about it. For the AI, it's all just context.
Here's what read_note returns when it can't find a note (from src/basic_memory/mcp/tools/read_note.py — the pattern dates back to the earliest versions):
# Note Not Found: "coffee brewing methods"
I couldn't find any notes matching "coffee brewing methods". Here are some suggestions:
## Check Identifier Type
- If you provided a title, try using the exact permalink instead
- If you provided a permalink, check for typos or try a broader search
## Search Instead
Try searching for related content:
search_notes(project="main", query="coffee brewing methods")
## Recent Activity
Check recently modified notes:
recent_activity(timeframe="7d")
## Create New Note
This might be a good opportunity to create a new note on this topic:
write_note(project="main", title="Coffee Brewing Methods", ...)
The "error" is really a menu of next moves for the model.
Keep the flow going
Give the model an idea of what to do next. When the search results return, the prompts returned to the model include instructions about reading the full results.
For example, the search prompt appends this right after the result list (from src/basic_memory/mcp/prompts/search.py):
## Next Steps
Based on these 5 results, you can:
1. **Read a specific note** - Use `read_note("permalink")` to see full content
2. **Build context** - Use `build_context("memory://path")` to see relationships
3. **Refine search** - Use `search_notes("refined query")` to narrow results
4. **Check recent activity** - Use `recent_activity(timeframe="7d")` for recent changes
Be liberal with inputs
This is an old programmer saying (Postel's law), but it's especially true with LLMs. If you make them pass in complex JSON, they will most likely screw it up a few times. This wastes tokens and slows everything down. This leads to a lot of parsing on the server end, but that is preferred because you can test it easily.
At this point, I felt like I had enough to share with more people. I had a few friends I set up with the v0.1 basic-memory MCP server and was shocked that without much help, it just started working when people used it with Claude. Now, you didn't need to manage a bunch of stuff in Claude Projects. You could just start new chats and reference previous topics without having to re-explain all the time. I think a big part of this working is that LLMs, particularly Claude, are good at "seeing", or more accurately inferring, the context between two topics. Using Basic Memory, they had just enough tool support to find more data when needed and pull it into the context window. It was far from perfect, but it was a good start.
Here's how the basic system worked.
The decision the whole story hangs on (Dec 2024)
From the git log:
- 2024-12-14: FastAPI app born (1ed4c72f) AND the MCP server rewired to stop touching the DB — forwarding every call to that app in-process via httpx ASGITransport (052ee403).
- Client factored into
mcp/async_client.py(353342a5, 12-25); old low-level server deleted (7322bb53, 2025-01-18); v0.1.0 ships 2025-02-07 (7c6ed53a). - Because tools spoke to an ASGI app rather than a database, the same tools could later point at a remote API. That is the seam.
The decision has its own napkin drawing. I found it in my own knowledge base — a design note dated 2024-12-13, the day before the decision commits landed, stored inside Basic Memory itself. The connector label — "ASGI Transport" — and the dependency injection box are both already there:
Backing up a couple of months — this decision actually came in week two, before v0.1 ever shipped. One thing missing from FastMCP though was the dependency injection (DI) in FastAPI. I've seen it get a lot of hate, but in my experience, writing factory code without DI really sucks, makes you write a ton of boilerplate, and can easily end up a tightly coupled mess. I've seen this in just about every language I've programmed in. Also, FastAPI is well understood and has some really great patterns for testing.
As I started to put the basic code outlines together for Basic Memory, it was pretty clear that what I mostly needed was plain ole service code, some file parsing, file IO and database code. All very non-AI. The only real AI part of the app was in the tools and even that was mostly Pydantic (I have since come to understand various patterns to make the tools more AI friendly and effective, see "Prompting Patterns" above). As I was building the db layer, and business services, I kept thinking "this would be so much easier in FastAPI".
I considered making an http backend service and having the tools call to a local endpoint, but I was worried that since MCP was very new, and still difficult to install and configure, that would be more than people would want to deal with. Running a daemon service locally is a chore only a developer would likely be willing to endure. So, I made an unconventional decision, one that I really hadn't seen anyone else doing in a real app. I decided to follow the pattern typically used for testing a FastAPI app - create an app instance and pass requests to it through an in-memory ASGI client. It worked great for tests, so why not for real life? I started doing it, and it worked really well. In fact it's still the pattern used in Basic Memory today.
AI (Claude Desktop, etc.)
│
│ MCP over stdio
▼
┌────────────── one process ───────────────┐
│ │
│ MCP server (FastMCP) │
│ └── tool: write_note(...) │
│ │ │
│ │ httpx AsyncClient │
│ │ ASGITransport(app) │
│ │ no socket, no port, │
│ │ no daemon │
│ ▼ │
│ FastAPI app (in memory) │
│ └── /knowledge /search /memory │
│ │ │
│ ▼ │
│ services ──► SQLite index │
│ ──► Markdown files │
│ │
└──────────────────────────────────────────┘
It works like this:
- Tools get called via MCP by an AI agent or stdio call
- A tool contains an httpx client configured to call an in-memory FastAPI endpoint
- The tool transforms its args into an http request and calls one or more endpoints, and handles the response
- The tool then outputs the response to the AI in a format as needed: markdown, text, json
The benefits of this kind of setup became:
- Most of the application is just a FastAPI app: services, data repositories, db models, pydantic schemas
- This part of the application is really easy to test without needing any AI setup
- Tool function implementations were very small and just composing inputs and outputs
- It naturally fit into how you might build a CLI client to call a remote service too, so adding CLI support was easy
It did come with some extra overhead of doing Pydantic twice in the flow, once for tool args, and another time for the in memory api call, but in practice this was not the slow part. File parsing and IO were always the bigger bottleneck. And it turns out that simpler tool args work better than complex json anyway (to make the same point again).
I had planned to figure out a way to leverage this tool-to-endpoint proxy pattern for a cloud service. At that time in early MCP days, the only option for remote calls was SSE. This came with a bunch of problems, because it meant that the connection was stateful and connections had to be routed to a particular service instead of load balancing like you would typically do for a web application. But before I could get far enough here, they added streamable HTTP to the MCP spec (the 2025-03-26 revision, which also added the OAuth 2.1 authorization framework).
Going Pro
I'll take a quick detour and mention that between the time when I had the basic-memory MCP working locally and the Basic Memory Cloud (described below), I made an aborted attempt at a standalone application, Basic Memory Pro. The idea was to "break out of the box" of just being an MCP tool and control the entire UX for the application. This took the form of a Tauri (Electron in Rust) application shell with a React web editor and UI shell. The basic-memory app ran locally via a sidecar and exposed the FastAPI endpoints (the same ones the tools used) to the frontend app. I spent about a month on this and got to an ok-ish place. It was primitive, but using shadcn and Claude to code the frontend got me pretty far.
The experience left me with some lessons learned.
Only break one law at a time
There's an old saying, "Only break one law at a time", that I think translates to programming - only do one thing you aren't familiar with at a time. Trying to build a complex UI flow in React (typescript), in Tauri (Rust), for multiple platforms was just too much extra cruft to manage. The version of Tauri I was using (2.0, released October 2024) was newer, and Claude preferred coding in the old version. I wasn't familiar enough with Rust to review the code for correctness and churned a lot debugging.
Packaging is hell
Also, trying to bundle all of this together and make it work across platforms is non-trivial, to say the least, involving native packaging, uv for python, etc. Getting stuck at the last mile is the worst way to end a trip, but sometimes you just have to listen when the world is telling you something.
About this time, the MCP SDK got another rev (2025-05-08), this time enabling streamable HTTP and OAuth. This led me to re-evaluate my plans, in light of the troubles with the Pro app and consider a cloud product.
The payoff + the caveman cloud (Jun–Aug 2025)
From the git log:
- The founding decision cashes in: core's
create_client()starts branching local-ASGI vs cloud-HTTP on config ([473f70c9](https://github.com/basicmachines-co/basic-memory/commit/473f70c9), 2025-07-07). - Founding cloud shape (
BASIC_MEMORY_CLOUD_v2.md, 2025-06-15): one Fly app per tenant, encrypted Fly volumes, a separateapps/mcpgateway (SSE) + OAuth server. - Control-plane DB moved Supabase → Neon (
ee88ad110, 2025-08) — separate from and earlier than per-tenant Neon. - Queue engine at this point: DBOS.
Here it is in eleven lines (src/basic_memory/mcp/async_client.py, 473f70c9, 2025-07-07 — condensed; full version in the commit):
def create_client() -> AsyncClient:
config = ConfigManager().load_config()
if config.api_url:
# Use HTTP transport for remote API
return AsyncClient(base_url=config.api_url)
else:
# Use ASGI transport for local API
return AsyncClient(
transport=ASGITransport(app=fastapi_app),
base_url="http://test"
)
From the notes:
- The cloud was designed inside Basic Memory itself. The founding architecture note still lives in my knowledge base — a Basic Memory note, frontmatter and all (May 2025). The first drawing: React frontend, a Platform API / Basic Memory API split, Supabase Postgres for tenant management, Turso LibSQL for the knowledge DB, and git-backed file storage on Fly.
- Turso was in the very first sketch — it would be spiked for real in September and abandoned in October (a later chapter). The git-backed storage idea kept echoing back later too.
- The cloud repo's first commit is 2025-05-12; the design notes were imported a week later (
a0f3b3b9d, 2025-05-20) — already into adocs/archive/folder, which tells you how fast the ideas were churning. - The shape that actually got built arrived a month later:
BASIC_MEMORY_CLOUD.md(46f0d4563, 2025-06-14), thenBASIC_MEMORY_CLOUD_v2.md(4a5e761e1, 2025-06-15) — one Fly app per tenant.
So, I started thinking about what shape a Basic Memory Cloud could take. There were a few ideas to wrap my head around.
Local First to Privacy First
First of all, being in the cloud meant that things were no longer going to be "Local First". This had been a raison d'être until that point, so it required a shift in mindset. The truth is that local is cool, and gives a lot of benefits (privacy, ownership, control), but also has some real limitations. I really wanted to be able to use Basic Memory for every AI, and every surface: Claude Desktop, Claude Code (when it was released), mobile, but also ChatGPT. Only having notes local to one computer leaves you to manage all the syncing and sharing. Some people are ok with that, but I knew most people weren't. Even I couldn't be bothered to do it, and I knew how to make it all work. So, I tried to translate the principles for "Local First (only)" to a cloud context.
Privacy First
If I couldn't do local first, at least I could try to make things as private as possible. My goal was to find some sort of completely encrypted way to store notes (zero trust), but as I learned, this is also not really practical. Sharing means compromising control, and there are always tradeoffs. But I could design the cloud architecture to minimize them.
Tenant isolation
When designing a multitenant (tenant means customer in SaaS terminology) cloud architecture, you can either co-locate data, meaning keep it all together, or keep each customer completely isolated. There is no right or wrong answer, each has trade-offs. The easiest way tends to be co-locating, because managing services or data per customer can be a challenge. Nevertheless, after looking into some of the newer cloud hosting platforms, particularly fly.io, I decided to bias towards complete isolation. This is something that would evolve, in practice, but customer data has and will always be completely isolated.
Cost effective
The other key factor in managing a cloud service is designing for cost. Running servers in the cloud costs real money. Storing data in the cloud can also get expensive quickly. You even have to consider bandwidth and IO costs. You pay for all of it, and therefore cost has to factor heavily into your design, or you won't have a viable business.
Now we are in business
If I could get something working so running Basic Memory in the cloud was easier than running locally, and provided more features, I was hopeful I could find customers. After making a proof of concept, and thinking this could really work, I needed to have a proper business. Going into business is no small effort, and I knew it was more work than I wanted (or was able) to do on my own. I needed partners. Forming a team like Voltron is a story unto itself. The tl;dr is that I already had the crew lined up. I just had to figure out how everything was going to work.
What type of business do we want to be?
I've had a pretty long run as a working software developer (engineer). From consulting, to big companies, to startups, acquisition, promotion, manager, to getting laid off. I can hardly count how many times I've been laid off in my career. I know it takes two hands.
So when I thought about starting my own business, I had a lot of my own ideas. I had gotten as close as I had come to AI psychosis, in a conversation with Claude about how it could all work, producing several revisions of what we called the "manifesto". It's a bit too cringy to share, but there are some points I wanted to make sure were included:
- Tools shape thought — keep tools simple but powerful; let them enhance rather than replace human capabilities
- Knowledge belongs to people — store everything locally first, open formats (Markdown), no vendor lock-in, portable data
- DIY means freedom — build with proven technology (SQLite, Git), keep the architecture clear, share knowledge freely
- Human-AI collaboration works — each works in their preferred way, tools bridge the gap, learning is mutual
- Elegance through simplicity — the best solutions are often the simplest
I knew I wanted to maintain control, and lean towards the bootstrap method, rather than chase quick VC or angel funds. But I knew that way was going to be the slow, hard way.
Aside: Licensing
Another very real issue to reckon with was licensing. I had very adamantly decided to release Basic Memory under the AGPL 3.0 license. I'm a long time FOSS true believer, and it had always been a goal of mine to produce something I thought was worthwhile enough to share. But, I also wanted to make a business of this thing now. That's not impossible, many companies are built on FOSS, they just don't usually have the mega valuations and VC bucks. That does mean, however, to use the source code commercially, there are considerations. What I ended up doing, with advice from my lawyer, is licensing the Basic Memory to myself (via an LLC) with a proprietary license. This keeps the cloud IP clean, but still allows the commercial end to not worry about releasing code also.
NOTE: It's actually a goal of mine to open source as much code as possible back into Basic Memory. But the fact is that's quite a bit of work to make it ready for other people's eyes. So in practice, it's easier to build the cloud infra in private, then move stuff over after its more stable.
AI Memory is the TodoMVC of vibecoded apps
I'll speak to the elephant in the room. AI memory apps are the vibe-coded equivalent of the web TodoMVC. Everybody tries it, and there are tons in every imaginable language and framework. Some even have good or novel ideas. Most are also free and open source. But the thing is, AI memory (or more generally context management for an LLM) is a very, very hard problem, once you get past the easy parts.
On top of that, making an application (or service) that works across models, harnesses, platforms is a challenge. The type of challenge that is full of the non-fun problems to solve, platform issues, version incompatibilities, character encoding, file parsing, and synchronization. And all of that has to "just work", or your memory product is only good for you, not something lots of people will want to use.
And further on top of that, the type of people that tend to want and build this product are usually trying to use some fancy tool. Use a graph db, put everything in a vector store, see how fast my retrieval score is? Look it passed this benchmark with better scores than everyone (sometimes hardcoding the results). And, at the end of the day, you still end up with another black box.
Your AI can see your memory knowledge, but can you? If something is wrong, how do you know? What if you want to fix it, how easy is that? What if my AI keeps adding stuff to it and it just grows and grows, how well does it work? Solving all of these problems is much harder than vibe coding an app and calling it done. The hard problems are hard, even when the quick solution is easy.
The grind
Since starting writing most of this post, I have also re-implemented the web note editor for Basic Memory, added comments via critic markup, threw out the y.js/hocus-pocus feature that allowed live collaborative editing, fixed how our mermaid svgs were rendering, and addressed some weird issue where CodeMirror would make frontmatter bold if it wasn't formatted properly. I think of this work as the necessary, but not sufficient work of making a product. Its not what I wanted to deal with, but it was there in my way, so I had to fix it. This is what makes trying to put out a complete product so exhausting. But if you can't dogfood your own product, how can you expect others to use it? So that's life, I guess.
Afterward
It is a part of life, but the reality, is it wears you down. When I'm feeling burnt out from doing yet another arbitrary task, I start to think "why am I doing this, anyway?". For me it's personal, Basic Memory is my best idea for what to put out in the world right now. Its the thing I really care most about. So its a kind of big mix of a self-expression art piece (conception, design and execution is the act of creating art), combined with a science project (what happens if we mix these things together?), combined with a political statement (how I decide to present my work to be received - open source, user controlled data, no customer exploitation). It is through this process of self actualizing via creation that literally makes meaning by manifesting internal ideas externally. And that is an idea that keeps me motivated.
I get really pumped thinking of all the cool stuff I want to do, and somehow, with help from a team of awesome folks, and a lot of AI assistance, it is seemingly possible. I appreciate all the users and feedback we have gotten so far, and the chance its given me to connect with new people, literally every day. And on top of that, seeing what people have built with Basic Memory that I didn't even think was possible has actually struck me with awe. When I started, Basic Memory seemed like such a small thing, and in the scope of "Big AI" and "Big Tech" it will always be. But to me its been just one small thing after another that keeps building into a bigger and bigger thing.
Next up
- The storage struggle
- The unified tenant model
- Making it fast
- The circle closes: consolidation back into core
