MongoDB vs FerretDB

MongoDBFerretDBDatabases

You want MongoDB's document model and query API. The flexible schemas, the nested documents, the aggregation pipeline. But there's a catch. Maybe your company's legal team flagged the SSPL license. Maybe you're already running PostgreSQL and your ops team doesn't want to manage a second database system. Maybe you just want a document API without locking into MongoDB's ecosystem.

FerretDB promises exactly that: the MongoDB wire protocol on top of PostgreSQL. Same mongodb:// connection string, same MongoClient driver, same query syntax. Your documents get stored as JSONB in Postgres tables, so you keep your existing backup scripts, monitoring, and replication setup. Apache 2.0 licensed, no restrictions.

The pitch sounds great. The question is how well it actually works, and where the gaps are. We'll spin up both engines, run identical code against each one, and find out.

Contents

Quick Comparison

MongoDBFerretDB
Wire ProtocolNativeCompatible (implements MongoDB protocol)
Storage EngineWiredTiger (B-tree + LSM)PostgreSQL
LicenseSSPLApache 2.0
Query LanguageMongoDB Query Language (full)MongoDB Query Language (subset)
AggregationFull pipeline supportCore stages ($group, $sort, $match, $project, $unwind, $limit, $skip, $count)
Change StreamsYesNot yet
$lookup (joins)YesNot yet
Text SearchBuilt-in text indexesNot yet (use PostgreSQL full-text search directly)
TransactionsMulti-document (explicit sessions)PostgreSQL ACID (default)
Backup Toolingmongodump / Atlas snapshotspg_dump, pg_basebackup, PITR
ShardingBuilt-inVia PostgreSQL (Citus, partitioning)
Drivermongodb npm packageSame mongodb npm package

The important row: the driver is the same. Your application code is identical. The differences are in what happens after the query leaves your process.

Set Up Both Locally

We'll use SpinDB to run MongoDB and FerretDB side by side. No Docker, no manual configuration. (What is SpinDB?)

Install SpinDB if you haven't:

bash
npm i -g spindb    # npm
pnpm add -g spindb # pnpm

Create both instances:

bash
spindb create mongo1 -e mongodb --start
spindb create ferret1 -e ferretdb --start

SpinDB downloads the correct binaries, configures each engine, and starts the servers. FerretDB's setup also provisions the PostgreSQL backend automatically.

Check they're running:

bash
spindb url mongo1
spindb url ferret1
text
mongodb://127.0.0.1:27017
mongodb://127.0.0.1:27018

Two different ports, two different storage engines, same wire protocol.

Set Up the Project

bash
mkdir mongo-vs-ferret && cd mongo-vs-ferret
pnpm init
pnpm add mongodb
pnpm add -D tsx typescript

Create a file called compare.ts. All the code in this post goes into that one file.

The Same Driver, Two Backends

This is the whole point of the comparison. You write one set of queries using the standard mongodb npm package, and it works against both engines. Here's a dataset of tasks for a project management app:

typescript
const tasks = [
  {
    title: 'Set up CI pipeline',
    project: 'Backend',
    priority: 'high',
    status: 'done',
    assignee: 'Alice',
    tags: ['devops', 'automation'],
    hoursEstimated: 8,
    hoursActual: 6,
  },
  {
    title: 'Design landing page',
    project: 'Frontend',
    priority: 'high',
    status: 'in-progress',
    assignee: 'Bob',
    tags: ['design', 'ui'],
    hoursEstimated: 12,
    hoursActual: 9,
  },
  {
    title: 'Write API documentation',
    project: 'Backend',
    priority: 'medium',
    status: 'in-progress',
    assignee: 'Charlie',
    tags: ['docs', 'api'],
    hoursEstimated: 6,
    hoursActual: 4,
  },
  {
    title: 'Add dark mode support',
    project: 'Frontend',
    priority: 'low',
    status: 'todo',
    assignee: 'Bob',
    tags: ['ui', 'feature'],
    hoursEstimated: 4,
  },
  {
    title: 'Optimize database queries',
    project: 'Backend',
    priority: 'high',
    status: 'todo',
    assignee: 'Alice',
    tags: ['performance', 'database'],
    hoursEstimated: 10,
  },
  {
    title: 'Set up error tracking',
    project: 'Backend',
    priority: 'medium',
    status: 'done',
    assignee: 'Charlie',
    tags: ['devops', 'monitoring'],
    hoursEstimated: 4,
    hoursActual: 5,
  },
  {
    title: 'Build notification system',
    project: 'Frontend',
    priority: 'medium',
    status: 'todo',
    assignee: 'Alice',
    tags: ['feature', 'ui', 'api'],
    hoursEstimated: 16,
  },
  {
    title: 'Migrate to TypeScript',
    project: 'Frontend',
    priority: 'high',
    status: 'in-progress',
    assignee: 'Charlie',
    tags: ['refactor', 'typescript'],
    hoursEstimated: 20,
    hoursActual: 14,
  },
]

Some tasks have hoursActual, some don't. That kind of schema flexibility is natural in a document database.

Connect to both engines and run identical operations:

typescript
import { MongoClient } from 'mongodb'

const MONGO_URL = 'mongodb://localhost:27017'
const FERRET_URL = 'mongodb://localhost:27018'

async function runQueries(url: string, label: string) {
  const client = new MongoClient(url)
  await client.connect()

  const db = client.db('projectmgmt')
  const collection = db.collection('tasks')

  // Clean up from previous runs
  await collection.drop().catch(() => {})

  // Insert
  const inserted = await collection.insertMany(tasks)
  console.log(`\n[${label}] Inserted ${inserted.insertedCount} tasks`)

  // Query: high-priority tasks that aren't done
  const urgent = await collection
    .find({ priority: 'high', status: { $ne: 'done' } })
    .sort({ hoursEstimated: -1 })
    .toArray()

  console.log(`[${label}] High-priority, not done:`)
  for (const t of urgent) {
    console.log(`  ${t.title} (${t.assignee}) - ${t.hoursEstimated}h estimated`)
  }

  // Array query: tasks tagged with "ui"
  const uiTasks = await collection
    .find({ tags: 'ui' })
    .toArray()

  console.log(`[${label}] Tasks tagged "ui":`)
  for (const t of uiTasks) {
    console.log(`  ${t.title} - tags: ${t.tags.join(', ')}`)
  }

  // Aggregation: hours estimated by project
  const projectStats = await collection
    .aggregate([
      {
        $group: {
          _id: '$project',
          totalEstimated: { $sum: '$hoursEstimated' },
          taskCount: { $sum: 1 },
        },
      },
      { $sort: { totalEstimated: -1 } },
    ])
    .toArray()

  console.log(`[${label}] Hours estimated by project:`)
  for (const stat of projectStats) {
    console.log(`  ${stat._id}: ${stat.totalEstimated}h across ${stat.taskCount} tasks`)
  }

  // Update: mark a task as done
  await collection.updateOne(
    { title: 'Optimize database queries' },
    { $set: { status: 'done', hoursActual: 8 } },
  )

  const updated = await collection.findOne(
    { title: 'Optimize database queries' },
    { projection: { title: 1, status: 1, hoursActual: 1, _id: 0 } },
  )
  console.log(`[${label}] After update: ${updated?.title} - ${updated?.status}, ${updated?.hoursActual}h actual`)

  await client.close()
}

await runQueries(MONGO_URL, 'MongoDB')
await runQueries(FERRET_URL, 'FerretDB')
text
[MongoDB] Inserted 8 tasks
[MongoDB] High-priority, not done:
  Migrate to TypeScript (Charlie) - 20h estimated
  Design landing page (Bob) - 12h estimated
  Optimize database queries (Alice) - 10h estimated
[MongoDB] Tasks tagged "ui":
  Design landing page - tags: design, ui
  Add dark mode support - tags: ui, feature
  Build notification system - tags: feature, ui, api
[MongoDB] Hours estimated by project:
  Frontend: 52h across 4 tasks
  Backend: 28h across 4 tasks
[MongoDB] After update: Optimize database queries - done, 8h actual

[FerretDB] Inserted 8 tasks
[FerretDB] High-priority, not done:
  Migrate to TypeScript (Charlie) - 20h estimated
  Design landing page (Bob) - 12h estimated
  Optimize database queries (Alice) - 10h estimated
[FerretDB] Tasks tagged "ui":
  Design landing page - tags: design, ui
  Add dark mode support - tags: ui, feature
  Build notification system - tags: feature, ui, api
[FerretDB] Hours estimated by project:
  Frontend: 52h across 4 tasks
  Backend: 28h across 4 tasks
[FerretDB] After update: Optimize database queries - done, 8h actual

Same code, same output, different engines. runQueries has no idea which backend it's talking to.

Run it:

bash
npx tsx compare.ts

Where They Diverge

That was the happy path. Here's the honest part: FerretDB does not support everything MongoDB supports. Some operations will work on MongoDB and fail on FerretDB.

Text Search Indexes

MongoDB has built-in text indexes for keyword search:

typescript
// This works on MongoDB
await collection.createIndex({ title: 'text', tags: 'text' })

const results = await collection
  .find({ $text: { $search: 'database optimization' } })
  .toArray()

FerretDB doesn't support text indexes or the $text operator yet. For full-text search with FerretDB, you can query PostgreSQL directly using its tsvector/tsquery capabilities, or use a dedicated search engine like Meilisearch.

$lookup (Cross-Collection Joins)

MongoDB's $lookup stage lets you join documents from different collections inside an aggregation pipeline:

typescript
// This works on MongoDB, not on FerretDB
const withAssigneeDetails = await collection
  .aggregate([
    {
      $lookup: {
        from: 'users',
        localField: 'assignee',
        foreignField: 'name',
        as: 'assigneeInfo',
      },
    },
  ])
  .toArray()

FerretDB doesn't support $lookup yet. For cross-collection queries, you'll need to do them in application code (two separate queries joined in your app) or query the underlying PostgreSQL tables directly.

Change Streams

MongoDB supports real-time change notifications via change streams:

typescript
// This works on MongoDB, not on FerretDB
const changeStream = collection.watch()
changeStream.on('change', (change) => {
  console.log('Document changed:', change)
})

FerretDB doesn't support change streams. For real-time notifications with FerretDB, PostgreSQL's LISTEN/NOTIFY is the mechanism you'd use instead.

Other Gaps

A few more MongoDB features FerretDB doesn't support yet:

  • $graphLookup for recursive graph queries
  • Capped collections for fixed-size, insertion-order collections
  • $merge and $out aggregation stages for writing results to collections
  • Schema validation ($jsonSchema on createCollection)

The FerretDB team publishes a compatibility page that tracks which commands and operators are supported. Check it before migrating production workloads.

Key Differences

Beyond feature gaps, MongoDB and FerretDB make fundamentally different architectural choices.

Storage Engine

MongoDB uses WiredTiger, a purpose-built engine optimized for document workloads: B-trees, log-structured merge trees, built-in compression, document-level concurrency. Designed specifically for high write throughput, large working sets, and concurrent reads.

FerretDB stores documents as JSONB in PostgreSQL tables. Your data benefits from PostgreSQL's battle-tested storage, WAL-based crash recovery, and vacuum/autovacuum maintenance. The tradeoff: PostgreSQL wasn't designed for document workloads specifically, so write-heavy operations with large documents may perform differently.

Licensing

MongoDB's SSPL prevents you from offering it as a service without open-sourcing your entire stack. This affects cloud providers, managed database vendors, and companies building internal database-as-a-service platforms.

FerretDB's Apache 2.0 license has no such restrictions. Self-host, embed, modify, redistribute in commercial products. No additional obligations.

Operational Model

Running MongoDB means operating MongoDB: its own backup tools (mongodump, mongorestore), its own monitoring (mongostat, mongotop), its own replication protocol, its own sharding coordinator (mongos). That's a whole separate operational surface.

Running FerretDB means operating PostgreSQL. If your team already manages Postgres, you already know how to back it up, monitor it, replicate it, and scale it. Your existing pg_dump scripts, your Patroni cluster, your PgBouncer connection pooler: they all work with the data FerretDB stores. I find this really compelling if your team is already a Postgres shop.

Ecosystem

MongoDB has a massive ecosystem: Atlas (managed cloud), Compass (GUI), Charts (visualization), Realm (mobile sync), and deep integrations with every major framework and cloud provider.

FerretDB inherits PostgreSQL's ecosystem: pgAdmin, DBeaver, PostGIS, pgvector, pg_cron, and the ability to query your document data with plain SQL when you need to.

When to Pick MongoDB

Pick MongoDB when:

  • You need the full feature set. $lookup, $graphLookup, change streams, time series collections, client-side field-level encryption: MongoDB is the only option right now.
  • You want a managed cloud service. MongoDB Atlas is mature and polished, with global clusters, automated backups, search integration, and a generous free tier.
  • Document write performance is critical. WiredTiger is purpose-built for document storage. For write-heavy workloads or complex aggregation pipelines, MongoDB will likely outperform FerretDB.
  • You're building for the MongoDB ecosystem. Charts, Atlas Search, Realm: those services only work with MongoDB.
  • SSPL is not a concern. If you're building a product (not a database service) and your legal team is fine with SSPL, the licensing isn't a practical issue.

When to Pick FerretDB

Pick FerretDB when:

  • SSPL is a blocker. Self-hosting, embedding a database in your product, or building an internal platform? Apache 2.0 removes the licensing risk entirely.
  • You want MongoDB's API on PostgreSQL infrastructure. Your team knows Postgres. Your backups, monitoring, and scaling playbooks are built for it. FerretDB lets you add document storage without introducing a second database system.
  • You're migrating away from MongoDB. Swap the connection string, run your test suite, see what passes. For apps using core CRUD and basic aggregation, it's often straightforward.
  • You want PostgreSQL extensions alongside documents. PostGIS for geospatial queries, pgvector for embeddings, pg_cron for scheduled jobs. Documents and relational data in the same database.
  • You're prototyping. Start with the MongoDB API for flexible schemas. If you later move to structured PostgreSQL tables, your data is already there.

Layerbase Cloud

Both MongoDB and FerretDB are available on Layerbase Cloud. Create a managed instance and get a connection URL from the Quick Connect panel:

Cloud instances use TLS. The connection string works with the same MongoClient constructor:

typescript
const client = new MongoClient(
  'mongodb://user:pass@cloud.layerbase.dev:PORT/db?tls=true',
)

Wrapping Up

MongoDB and FerretDB share a wire protocol and query language but solve different problems. MongoDB gives you the full feature set with a purpose-built storage engine. FerretDB gives you the core MongoDB API backed by PostgreSQL, with an open-source license and a familiar operational model.

The code in this post shows that for standard CRUD, filtered queries, array matching, and basic aggregation, they produce identical results. The gaps are real (no $lookup, no text search, no change streams), but for many applications they won't matter.

Try both. Decide based on what your project actually needs, not what sounds impressive on paper.

Manage your local instances:

bash
spindb stop mongo1     # Stop MongoDB
spindb stop ferret1    # Stop FerretDB
spindb start mongo1    # Start MongoDB
spindb start ferret1   # Start FerretDB
spindb list            # See all your database instances

SpinDB supports 20+ engines, making it easy to run both side by side. For a hosted option, Layerbase Cloud provisions either engine in seconds.

Something not working?