Every developer has had this moment. You pick a database on day one (Firebase because it's fast, Mongo because it's flexible, Postgres because it's "the right choice") and six months later you'd pick differently. By then, the database has leaked into everything.
When I started building Underpeaks, a headless CMS that generates Flutter and Next.js apps from one data model, I made a decision that cost me far more time than I expected: it would run on five databases (Supabase, PostgreSQL, MySQL, MongoDB and Firebase) behind one interface.
Here's why, how it works, and what it actually cost.
Open source isn't the same as portable
"Open source" is table stakes for a CMS now. But open source doesn't mean you can leave. Most tools bake in one database, and switching means a rewrite: queries, auth, migrations, the lot.
I wanted the opposite. Your data model should be the source of truth, and the database should be a decision you can change your mind about.
One interface, five engines
Everything in Underpeaks Core talks to a single DBAdapter interface. Routes never import a database driver directly. They ask for the configured adapter:
ts
const adapter = getConfiguredAdapter()
const dbConfig = adapter.config
const users = await adapter.readAll(dbConfig, 'nxf_users')
Each adapter implements the same methods (create, read, readAll, update, delete, plus auth helpers) for its own engine. Swap the adapter, and the rest of the codebase doesn't know or care.
That's the easy part to describe. The hard part is that five databases disagree about almost everything.
Where the databases disagree
Primary keys. SQL tables often use id. But a model called products might declare product_id as its primary key, and Firebase documents have IDs that aren't fields at all. Every update and delete has to know which column is the real key, so the interface takes it explicitly:
ts
await adapter.update(dbConfig, 'products', productId, data, 'product_id')
Forget that last argument on a model whose key isn't literally id, and the update silently matches nothing. I learned that one the hard way.
Concurrency. Firing off parallel queries with Promise.all is harmless with Firebase or Supabase's HTTP client. With a pooled Postgres or MySQL connection, it can exhaust connections or interleave in ways you don't expect. The SQL adapters now read sequentially. It's slower on paper and far more predictable in practice.
Response shapes. One path returned { data }, another returned { records }. Generated apps expecting one shape silently got undefined from the other. Consistent envelopes aren't glamorous, but they're the difference between "it works" and "it works on my database."
Schema. SQL wants real tables and columns. MongoDB and Firebase are happy with whatever you send. The adapter layer has to create physical tables where the engine needs them, and enforce structure where the engine won't.
What it cost
Honestly? Much more than I planned. Supporting one database would have taken about [2-3 weeks]. Five took [4-5 months].
The cost isn't writing five adapters. It's that every feature now has five edge cases. Auth, file storage, pagination and filtering each work slightly differently per engine, and every bug has to be checked against all five. A fix that's correct for Postgres can be wrong for Firebase.
The trade-offs, honestly
A single-database tool can do things this approach can't:
Use engine-specific features freely. Postgres full-text search, Mongo aggregation pipelines, Firebase realtime listeners. An adapter layer has to target what all five share, or handle each specially.
Move faster. One engine means one set of edge cases.
Tune performance deeply for that one database.
If you know you'll only ever use Postgres, a Postgres-native tool is a perfectly good choice.
Who it's for
The adapter layer is for developers who:
already run a database and don't want to migrate to use a CMS
build for clients with different infrastructure
want the option to change their mind later without a rewrite
Database freedom as insurance
I don't think of multi-database support as a feature you use every day. It's insurance. Most projects will never switch databases, but knowing you can changes how comfortably you commit.
Underpeaks Core is open source and self-hosted. Pick your engine, and keep the option to change your mind.
👉 Try Underpeaks Core
Github: Underpeaks_core
Top comments (0)