Getting Started with FerretDB
MongoDB's document model and query API are excellent. Flexible schemas, nested arrays, expressive query operators, massive ecosystem. But MongoDB changed to a non-open-source license (SSPL), which creates real problems for companies that need to self-host or embed a document database without licensing risk.
FerretDB solves this. It implements the MongoDB wire protocol on top of PostgreSQL, so your existing MongoDB code (drivers, ODMs, tools like mongosh) works without changes. Under the hood, documents live in PostgreSQL: ACID transactions, mature backup tooling (pg_dump), and the entire PostgreSQL extension ecosystem. No SSPL, no license concerns.
We'll build a product catalog in one TypeScript file: inserting documents, running queries, filtering arrays, and building an aggregation pipeline. Follow along locally, or create a managed instance on Layerbase Cloud.
Contents
- Create a FerretDB Instance
- Set Up the Project
- The Product Dataset
- Connect and Insert
- Query with Filters
- Array Matching
- Aggregation Pipeline
- Regex Search
- The PostgreSQL Advantage
- When to Use FerretDB
- Wrapping Up
Create a FerretDB Instance
Local with SpinDB
SpinDB gets FerretDB (and its PostgreSQL backend) running with a single command. No Docker, no manual Postgres setup. (What is SpinDB?)
Install SpinDB globally:
npm i -g spindb # npm
pnpm add -g spindb # pnpmOr run it directly without installing:
npx spindb create ferret1 -e ferretdb --start # npm
pnpx spindb create ferret1 -e ferretdb --start # pnpmIf you installed globally, create and start a FerretDB instance:
spindb create ferret1 -e ferretdb --startSpinDB downloads FerretDB and its PostgreSQL backend, configures both, and starts the server. Verify it's running:
spindb url ferret1mongodb://127.0.0.1:27017Standard MongoDB connection URL. Any MongoDB client or driver connects to it without knowing FerretDB is behind it. Leave the server running.
Layerbase Cloud
Layerbase Cloud provisions managed FerretDB instances. Pick FerretDB from the engine list and grab your connection URL from the Quick Connect panel.
Cloud instances use TLS, so the connection string looks like this:
const client = new MongoClient(
'mongodb://user:pass@cloud.layerbase.dev:PORT/db?tls=true',
)Everything else in this guide works identically whether you're running locally or on Layerbase Cloud. Just swap in your connection details.
Set Up the Project
mkdir ferretdb-products && cd ferretdb-products
pnpm init
pnpm add mongodb
pnpm add -D tsx typescriptCreate a file called catalog.ts. All the code in this post goes into that one file.
Because FerretDB speaks the MongoDB wire protocol, the standard mongodb driver works without any adapter or plugin.
The Product Dataset
Fifteen products for an e-commerce store. Nested arrays and mixed types, the kind of flexible documents that make a document database useful:
const products = [
{
id: 1,
name: 'Wireless Earbuds Pro',
category: 'Electronics',
price: 79.99,
tags: ['wireless', 'bluetooth', 'audio'],
rating: 4.5,
inStock: true,
},
{
id: 2,
name: 'Running Shoes Ultra',
category: 'Sports',
price: 129.99,
tags: ['outdoor', 'running', 'lightweight'],
rating: 4.7,
inStock: true,
},
{
id: 3,
name: 'Cotton T-Shirt Classic',
category: 'Clothing',
price: 24.99,
tags: ['casual', 'cotton', 'basics'],
rating: 4.2,
inStock: true,
},
{
id: 4,
name: 'Smart Home Hub',
category: 'Electronics',
price: 149.99,
tags: ['wireless', 'smart-home', 'premium'],
rating: 4.3,
inStock: false,
},
{
id: 5,
name: 'Yoga Mat Premium',
category: 'Sports',
price: 49.99,
tags: ['indoor', 'premium', 'exercise'],
rating: 4.8,
inStock: true,
},
{
id: 6,
name: 'Ceramic Coffee Mug Set',
category: 'Home',
price: 34.99,
tags: ['kitchen', 'ceramic', 'gift'],
rating: 4.6,
inStock: true,
},
{
id: 7,
name: 'Bluetooth Speaker Portable',
category: 'Electronics',
price: 59.99,
tags: ['wireless', 'bluetooth', 'portable', 'outdoor'],
rating: 4.4,
inStock: true,
},
{
id: 8,
name: 'Hiking Backpack 40L',
category: 'Sports',
price: 89.99,
tags: ['outdoor', 'hiking', 'waterproof'],
rating: 4.5,
inStock: true,
},
{
id: 9,
name: 'TypeScript Programming Guide',
category: 'Books',
price: 39.99,
tags: ['programming', 'typescript', 'reference'],
rating: 4.9,
inStock: true,
},
{
id: 10,
name: 'LED Desk Lamp',
category: 'Home',
price: 44.99,
tags: ['lighting', 'adjustable', 'office'],
rating: 4.3,
inStock: true,
},
{
id: 11,
name: 'Noise Cancelling Headphones',
category: 'Electronics',
price: 249.99,
tags: ['wireless', 'premium', 'noise-cancelling', 'audio'],
rating: 4.7,
inStock: false,
},
{
id: 12,
name: 'Wool Winter Jacket',
category: 'Clothing',
price: 189.99,
tags: ['winter', 'wool', 'premium'],
rating: 4.4,
inStock: true,
},
{
id: 13,
name: 'Stainless Steel Water Bottle',
category: 'Sports',
price: 29.99,
tags: ['outdoor', 'reusable', 'insulated'],
rating: 4.6,
inStock: true,
},
{
id: 14,
name: 'Smartphone Stand Adjustable',
category: 'Electronics',
price: 19.99,
tags: ['desk', 'adjustable', 'portable'],
rating: 4.1,
inStock: true,
},
{
id: 15,
name: 'Scented Candle Collection',
category: 'Home',
price: 27.99,
tags: ['relaxation', 'gift', 'aromatic'],
rating: 4.5,
inStock: true,
},
]Each product has a tags array of variable length, plus a mix of numbers, strings, and booleans. In a relational database you'd need a separate product_tags junction table. Here, it's just a field.
Connect and Insert
Connect with the standard MongoDB driver and insert the dataset:
import { MongoClient } from 'mongodb'
const client = new MongoClient('mongodb://localhost:27017')
await client.connect()
const db = client.db('store')
const collection = db.collection('products')
// Clean up from previous runs
await collection.drop().catch(() => {})
const result = await collection.insertMany(products)
console.log(`Inserted ${result.insertedCount} products`)Identical to what you'd write for MongoDB. MongoClient connects to port 27017, gets a database reference, and inserts documents. FerretDB accepts these commands over the wire protocol and translates them into PostgreSQL operations under the hood.
Query with Filters
Field matching, comparison operators, logical combinations. All work through FerretDB:
// Electronics under $100
const cheapElectronics = await collection
.find({ category: 'Electronics', price: { $lt: 100 } })
.toArray()
console.log('\nElectronics under $100:')
for (const p of cheapElectronics) {
console.log(` ${p.name} - $${p.price}`)
}
// High-rated products that are in stock
const topRated = await collection
.find({ rating: { $gte: 4.5 }, inStock: true })
.sort({ rating: -1 })
.toArray()
console.log('\nTop rated (4.5+), in stock:')
for (const p of topRated) {
console.log(` ${p.name} - ${p.rating} stars`)
}Electronics under $100:
Wireless Earbuds Pro - $79.99
Bluetooth Speaker Portable - $59.99
Smartphone Stand Adjustable - $19.99
Top rated (4.5+), in stock:
TypeScript Programming Guide - 4.9 stars
Yoga Mat Premium - 4.8 stars
Running Shoes Ultra - 4.7 stars
Ceramic Coffee Mug Set - 4.6 stars
Stainless Steel Water Bottle - 4.6 stars
Wireless Earbuds Pro - 4.5 stars
Hiking Backpack 40L - 4.5 stars
Scented Candle Collection - 4.5 stars$lt, $gte, field matching, .sort(). All work exactly as they do in MongoDB.
Array Matching
One of MongoDB's most useful features is how it handles arrays. $in matches documents where an array field contains any of the specified values:
// Products tagged "wireless" or "bluetooth"
const wirelessProducts = await collection
.find({ tags: { $in: ['wireless', 'bluetooth'] } })
.toArray()
console.log('\nWireless or Bluetooth products:')
for (const p of wirelessProducts) {
console.log(` ${p.name} - tags: ${p.tags.join(', ')}`)
}Wireless or Bluetooth products:
Wireless Earbuds Pro - tags: wireless, bluetooth, audio
Smart Home Hub - tags: wireless, smart-home, premium
Bluetooth Speaker Portable - tags: wireless, bluetooth, portable, outdoor
Noise Cancelling Headphones - tags: wireless, premium, noise-cancelling, audioNo joins, no junction tables, no subqueries. The array is a first-class citizen in the query language.
Aggregation Pipeline
The aggregation framework supports multi-stage data transformations. Group products by category, calculate average price and count:
const categoryStats = await collection
.aggregate([
{
$group: {
_id: '$category',
avgPrice: { $avg: '$price' },
count: { $sum: 1 },
},
},
{ $sort: { avgPrice: -1 } },
])
.toArray()
console.log('\nCategory statistics:')
console.log(' Category Avg Price Count')
for (const stat of categoryStats) {
const avg = (stat.avgPrice as number).toFixed(2)
console.log(` ${String(stat._id).padEnd(14)} $${avg.padStart(7)} ${stat.count}`)
}Category statistics:
Category Avg Price Count
Electronics $111.99 5
Clothing $107.49 2
Sports $74.99 4
Books $39.99 1
Home $35.99 3$group collects documents by category, $avg computes the mean price, $sum: 1 counts per group. Same aggregation pipeline syntax MongoDB developers already know.
Regex Search
Regex matching on string fields works through FerretDB too:
// Find products with "phone" in the name (case-insensitive)
const phoneProducts = await collection
.find({ name: { $regex: /phone/i } })
.toArray()
console.log('\nProducts matching "phone":')
for (const p of phoneProducts) {
console.log(` ${p.name} - $${p.price}`)
}Products matching "phone":
Noise Cancelling Headphones - $249.99
Smartphone Stand Adjustable - $19.99Run the full script:
npx tsx catalog.tsField filters, array matching, aggregation pipeline, regex search: all standard MongoDB operations. The driver has no idea it's talking to FerretDB. Every one of these documents lives in PostgreSQL.
The PostgreSQL Advantage
FerretDB isn't just "open-source MongoDB." Your data lives in PostgreSQL, and that buys you real things:
Default ACID transactions. PostgreSQL wraps every operation in a transaction by default. MongoDB added multi-document ACID in 4.0, but it requires explicit sessions and is opt-in. With FerretDB, transactional behavior is the default.
Mature backup and recovery. pg_dump, pg_basebackup, point-in-time recovery. Decades of production use across millions of deployments.
The PostgreSQL extension ecosystem. PostGIS, pg_cron, pgvector, and hundreds of others. Your document data coexists with all of it.
No SSPL licensing concerns. FerretDB is Apache 2.0. Self-host, embed, modify, redistribute, no restrictions.
Unified infrastructure. If you already run PostgreSQL, FerretDB lets you add document storage without a separate database system. One backup strategy, one set of credentials, one operational playbook.
When to Use FerretDB
FerretDB is the right fit when:
- You want MongoDB's query API but PostgreSQL's reliability. The document model and query operators are genuinely useful. You just don't want a separate database system with its own operational overhead.
- You're migrating off MongoDB's license. Existing code, drivers, and ODMs (like Mongoose) work without changes. Swap the connection string and test.
- Your team knows MongoDB but your infrastructure is PostgreSQL. No retraining, no new ops tooling. Same queries they already know.
- You need flexible schemas for prototyping. Start schemaless, then add validation or move to structured tables later. All within PostgreSQL.
- Licensing matters. Apache 2.0 vs. SSPL is a meaningful difference for companies that self-host or distribute software.
Wrapping Up
Under 80 lines of real code. Standard MongoDB driver, document insertion, filtered queries, array matching, aggregation pipeline, regex search. None of that code knows or cares that FerretDB is translating every operation into PostgreSQL.
The FerretDB documentation covers supported commands, compatibility details, and configuration.
To manage your local FerretDB instance:
spindb stop ferret1 # Stop the server
spindb start ferret1 # Start it again
spindb list # See all your database instancesSpinDB supports 20+ engines, so you can run FerretDB alongside CockroachDB, Redis, or DuckDB without juggling separate installs. Prefer a GUI? Layerbase Desktop provides a macOS app for the same operations.