How I picked a database for Letopis
From the moment I learned MongoDB existed, I fell for it — the document model, the flexible schema, the syntax itself. For any fitting task I reached for Mongo and rarely regretted it: on day one it means zero migration pain and maximum speed. So when I sat down to build Letopis — a personal library of knowledge that lives in the cloud, packs everything up, searches by meaning and files itself away — I was sure Mongo would be at the core.
Production runs PostgreSQL. This isn't a compromise I made through gritted teeth; it's a decision I'm now ready to defend with numbers. Here's how I got there, including the places where Postgres loses — I don't hide those.
First, not "which database" but "whose"
I made the first call before I opened a single price list: the database lives with an external provider. Managed, not my own box on a VPS.
Behind that is a simple split of what I'm willing to deal with and what I'm not. I want to write the product, not babysit pg_dump at three in the morning when a VM loses its disk. Managed gives me two things this was all about:
- Less administration. Backups, version upgrades, fault tolerance and easy migration are on the provider. If they disappoint, I drag the dump elsewhere instead of rebuilding infrastructure from scratch.
- The database outlives the server. If the VPS dies or gets hacked, the database stays alive and well — it sits physically apart from it. That it stays fine is now on the provider's conscience, not mine :)
There's a reason that matters more: shared responsibility for user data. When you sign a DPA with Standard Contractual Clauses (SCC) with an EU provider, as a processor it takes on the infrastructural and legal side of storing personal data. What's left on me is the application layer: access control, deletion on request, scrubbing personal data before it goes to an LLM. Not "all of GDPR on me," but the half my code covers. For a solo developer, that makes the difference.
Where to host it, and why not the CIS
Next, geography. I looked at the EU or the US right away: that's where my market is (Western Slavs — Poland, Czechia, Slovakia — plus Eastern Europe), with clear GDPR rules.
Out of curiosity I did check CIS providers, and here's what put me off for good. Take Yandex Cloud and their Managed Service for MongoDB. Turns out under the hood it isn't real MongoDB but StoreDoc — their own Mongo-compatible engine that merely speaks the Mongo protocol, versions six through eight. That honestly surprised me: you think you're getting managed Mongo and you get a renamed engine on an older protocol. There's no permanent free tier — you pay from day one, from the first megabyte; there's only a trial grant for a couple of months, but that's a trial, not freemium.
And "managed" there is managed in name only. A lot is done by hand over SSH; you can install the extension you need, but it's barely less hassle than standing up a database locally yourself. I wanted to get away from administration, and I was offered the same administration, just for money.
But what finished it for me wasn't price or SSH — it was the feature set. My task needs vector search, and you can't add it to that Mongo: there's no native $vectorSearch on the available versions, and you can't bolt it on. Not "more expensive" and not "an older version" — the capability simply isn't in the engine.
And it's not one provider's quirk. Even where everything is deliberately built on Russian infrastructure under data-localization laws, vectors still get pushed out into a separate PostgreSQL with pgvector, with Mongo left for text only. The takeaway is simple: the moment a task needs vectors, a document database stops coping on its own, and the road leads to pgvector. Hold that thought, it comes back.
The result: the EU, Frankfurt, GDPR residency out of the box. And already at this step it's clear that choosing a jurisdiction is a legal decision first and a technical one second.
What the database actually has to do
To follow the rest, you need to know my workload.
Letopis doesn't just search for similar chunks of text. It pulls entities and the links between them out of your notes and builds a knowledge graph from them — that's how the system understands context more deeply and answers questions where you have to walk the connections, not just find a similar paragraph. A knowledge graph means I need graph-shaped data work. (It's built on LightRAG, but that's an implementation detail, not the point.)
From the storage side the workload splits into four layers: cache and source texts, embeddings for semantic search, the graph topology itself, and document indexing status.
Under heavy load a zoo would be reasonable: Redis for cache, Pinecone for vectors, Neo4j for the graph — each tool in its own weight class. But that has its price: the multi-database tax (latency on network hops between services), endless synchronization, and three cloud bills instead of one. I chose not to obsess over architecture for architecture's sake but to get the product to a working state — better one live database than a pretty flying castle that never takes off. I had two candidates with real hands-on experience: MongoDB and PostgreSQL, in the shape of Supabase.
I wanted Mongo. Three blows
Honestly: I started with MongoDB Atlas. There's a free tier, I know Mongo like the back of my hand, everything pointed that way. Then I started counting and reading, and took three escalating blows.
Blow one: the free tier
The free Atlas cluster (M0) is half a gigabyte of disk, a hundred operations per second, and a ban on writing temporary data to disk in aggregations. There's also a widely cited limit on the number of search and vector indexes on the shared tier, around three; if it holds, I hit the ceiling immediately, because my architecture needs at least three vector indexes (for chunks, entities and links) on an empty database. But even without arguing about indexes, "half a gig plus a hundred ops per second plus no temp disk" is enough: a graph RAG doesn't live on that.
Blow two: prices
The jump from free to Atlas's first real dedicated tier (M10) is about 57 dollars a month, and that's the base figure: the community has a documented bill of over five hundred for the same M10 once backups, transfer and search pile on top. Supabase's move to Pro is 25 dollars. Half the entry price. And in Atlas's serverless model, where you pay per operation, something nastier surfaces: a write costs about ten times a read, and indexing a knowledge graph is all writes. I'm building a system that constantly appends to the knowledge base; for me that's a death sentence.
Blow three: Postgres turned out faster
This one broke my picture of the world. I was sure a graph needed a specialized graph database. I put together a benchmark on my own workload and ran three backends: recursive CTEs straight in Postgres, Neo4j and Apache AGE (the Postgres graph extension).
How I measured, to be fair: a real document graph of eight thousand nodes and twenty-five thousand edges, one-hop queries (neighborhood expansion, exactly how my system walks it), ten parallel workers; as a control I repeated it on a synthetic graph of eight thousand nodes and forty thousand edges — same picture. I tracked both throughput and response time.
| Graph backend | RPS, synthetic (8k/40k) | RPS, real (8k/25k) | p50 / p95, real | Dependencies |
|---|---|---|---|---|
| Postgres · recursive CTEs | 12,776 | 9,169 | 0.7 / 1.4 ms | any PostgreSQL 14+, zero external dependencies |
| Neo4j | 1,684 | 1,693 | 4.2 / 13.2 ms | dedicated graph server or AuraDB DBaaS |
| Apache AGE | 175 | 159 | 4.6 / 239 ms | custom DB image with AGE compiled in |
| MongoDB ☠ | — | — | — | no native graph; only $graphLookup |
Plain SQL in ordinary Postgres beat dedicated Neo4j by more than five times and crushed Apache AGE by nearly sixty. At my level of graphs.
That caveat — "at my level of graphs" — I'm obliged to say out loud. Were I building a system that finds shortest paths in graphs of millions of nodes with deep multi-hop traversals, Neo4j would bury Postgres: it stores links as direct pointers between nodes, and a deep traversal is almost free for it — on a tree of a third of a million nodes a recursive CTE takes 47 seconds where a native graph engine fits in 227 milliseconds. But my workload isn't that: the graph walks one or two hops out from nodes the vector search already found. Exactly where Postgres is strong and Neo4j is overkill.
Inside Postgres I'm glad I picked the right option in time. The obvious path is Apache AGE, but it's almost nowhere in managed-cloud catalogs: I'd have to drag a custom Docker image onto my own machine and lose all the zero-ops, plus it deadlocks under load. I went with recursive CTEs — just SQL over two ordinary tables, without a single external extension.
And Mongo on the graph field is worse still, which is why it gets a skull instead of numbers in the table. There's no native graph at all, only the traversal operator $graphLookup. In the canonical NoSQL benchmark from ArangoDB they had to drop $graphLookup entirely — it ran so slowly that they didn't even measure shortest paths on Mongo; MongoDB itself admits in the docs that $graphLookup "may not perform as well as a dedicated graph database." LightRAG can formally run on MongoDB as a backend, but the numbers above put the point of that in doubt.
An honest footnote about the numbers themselves: the benchmark above is mine, on the LightRAG integration, not an independent lab. I offer it as "here's what I measured at home," not as gospel.
Turns out Supabase is more than a database
While I was digging toward Postgres, things surfaced that I hadn't even been counting on.
Vectors. The pgvector extension does semantic search right inside the database. Modern embedding models produce vectors of such dimensionality that an ordinary index can't take them, and that's where the halfvec type helps (16-bit numbers instead of 32-bit): it halves the memory at the cost of about one recall point, and at large dimensionalities it's the only way to build an index at all.
By throughput a Postgres stack with a vector extension outruns even dedicated vector databases by multiples on large volumes — though, to be fair, on tail latency dedicated Qdrant still beats it.
| Metric (recall 99%, 50M vectors, 768d) | Postgres + vector extension | Qdrant |
|---|---|---|
| Throughput | 472 QPS | 41 QPS |
| Latency p50 | 31 ms | 31 ms |
| Latency p95 (lower is better) | 60 ms | 37 ms |
An honest caveat: those multiple-x throughput numbers are about an optimized vector extension (pgvectorscale in the Tiger Data benchmark), not the "plain" pgvector I'm running.
But at MVP scale — thousands and tens of thousands of fragments — that headroom is more than enough not just for me but for most products at the start. Paying for a separate vector service for tail latency I won't feel isn't my case.
Authentication. Supabase brings built-in Auth: 50,000 monthly active users free, 100,000 on Pro. For a solo that's one whole service gone — I don't have to stand up and maintain my own login provider.
And the main thing — it's all in one database. The note bytes themselves, vectors, graph, full-text search — in a single PostgreSQL, with ACID consistency. This isn't theory: I verified it in code on a live Supabase back on the prototype. One engine covers all four layers. Mongo can't do that, and Yandex, with its missing vector search, even less so.
Remember the thought from the second chapter, "the moment you need vectors, the road leads to pgvector"? There it closes.
How it ended
The favorite picked itself: Supabase, Frankfurt, Pro at 25 dollars a month. Vectors — pgvector with the halfvec type. Graph — recursive CTEs, no Apache AGE. Auth — built in. One bill, zero ops, EU residency out of the box.
I haven't fallen out of love with NoSQL. Non-relational databases seem to truly come into their own at large scale, when there's so much data the relational model starts to creak; and until I live there, Postgres in the shape of Supabase wins again. No hard feelings toward Mongo: just the right tool for the current scale. I moved to Postgres not because I stopped loving Mongo, but because for this particular task it was the better choice. Sometimes the most boring tool in the room is the right answer.
Prices and limits are as of June 2026. The graph-backend benchmarks are my own measurements on the LightRAG integration. If you're building something similar, don't take my word for it — run it on your own workload.