
MongoDB and Dolt were slammed together using coding agents. And the result is DumboDB.
Dolt is DoltHub’s first product, and it demonstrates the power of the Prolly Tree in a database. And it also has helped us understand how difficult it is to build a database that people trust. Databases are foundational to virtually every online application you have ever used, and the people who build them have come to expect certain features that are considered essential for any production system. We typically call these features Table Stakes, and not having them is usually a barrier to entry for many potential users.
One such feature in databases is the ability to backup. A well-designed backup and recovery process is a basic requirement for pretty much every business of reasonable size. It’s so common that many basic compliance badges spell it out explicitly. SOC 2 auditors look for data backup and recovery under its Availability criteria, ISO 27001 has a control named simply “Information backup”, and the HIPAA Security Rule requires a data backup plan for organizations handling protected health information. And auditors rarely stop at “do you have backups?” They want to see that you can restore from them and that you’ve actually tried. If your database can’t do that, neither can you.
It is reasonable to look at a distributed database which enables users to push and pull as much as they like and decide that backups aren’t a necessary feature. But that misses the point that, for the above-mentioned compliance requirements, it’s best to make meeting them as simple as possible.
So, with much delight, we announce that DumboDB now has a backup mechanism!
Background#
Before diving into an example, we should survey how Mongo and Dolt currently support backup.
With Mongo, there are two command line tools, part of MongoDB’s Database
Tools, called mongodump and mongorestore. These do exactly what they say: one
dumps the state of the database into a file archive, and the other
creates a new database from that archive.
A second mechanism which Mongo has which serves as kind of a backup is replication. It’s not as simple to convince the auditors that a replication cluster really is a backup though. There are additional features which are supported in Mongo’s paid offerings as well.
In Dolt, there is a
stored procedure,
dolt_backup(), which will push a copy of the database to a remote.
It uses the same plumbing as push and pull, and you can even clone
from a backup instance into a new database. The main thing which makes
a Dolt backup different from a remote repository is that the full state
of the database is captured - not just committed changes.
The distinction here is subtle, but since DumboDB follows this model, we
want to be clear. In a Dolt session, you can alter data and look at its
diff before deciding to commit it. Such changes are fully durable, and we
call them working set changes. If we pull back the covers a little bit,
there is a list of references to object ids.
# feature branch and its working set match. Clean Branch
refs/heads/feature #12rsfabhluiumueeqe1j87k8fr869il9
workingSets/heads/feature #12rsfabhluiumueeqe1j87k8fr869il9
# main branch differs from its working set. Dirty Branch
refs/heads/main #27cahsamt24abimvhvvt1s9jcceqgiir
workingSets/heads/main #1fob8pim5d2gsubr2i6fett2k4ovl8df
# Tags are references too!
refs/tags/v1 #27cahsamt24abimvhvvt1s9jcceqgiir
This map of branch state, names prefixed with refs/* or workingSets/*
and the objects they refer to, is called the root, and it’s critical to
how Dolt and DumboDB work. The root is the full handle to the database state.
In Dolt, and introduced today in DumboDB, the backup process preserves the entire root in a location which isn’t the main server. By preserving the root, we capture all of the data state at the time of the backup.
What Is Not Backed Up#
Similar to Dolt, DumboDB backups preserve the data of the database, but not the configuration. One notable piece of information which is lost in these backups is user logins and their permissions.
Restoring a database will get your data back, but there may be more work to do than just get your data back.
In DumboDB, all configuration (including user logins, roles, branch
configuration, and so forth) is stored in a special database
called admin, and that database can’t be pushed or backed
up. This is technically feasible, but we’ll wait for a user
to ask for it before we open that can of worms.
There is another gap in DumboDB which needs to be acknowledged,
which is that Dumbo commands don’t have fine-grained access control.
For the time being, running a server with --auth enabled will
prevent you from pushing, pulling, or backing up. We are working
to address this now.
Example#
All right. Enough talk. Now a demo!
We’re going to put some data in a database, back it up, lose the server entirely, and get everything back on a new one. This includes backing up the branch you were working on and the changes you hadn’t committed yet.
What You’ll Need#
Everything in the demo below was run on DumboDB v0.8.1. Backups are not in v0.8.0, so
you need the new binary. Grab the
v0.8.1 release; we
publish pre-built binaries for Linux, MacOS, and Windows on the
releases page, so drop the one
for your platform somewhere on your PATH. There’s a Docker image and
build-from-source instructions in the
README if you prefer either of
those.
You’ll also want mongosh, the
MongoDB shell, which is its own download separate from the server.
Run DumboDB#
In a terminal, start the server and leave it running:
$ dumbodb --data-dir /tmp/backupdemo/primary
time=2026-10-19T19:40:59.080Z level=INFO source=clientconn/listener.go:145 msg="Listening on TCP 127.0.0.1:27017..." name=listener
time=2026-10-19T19:40:59.080Z level=INFO source=dumbodb/main.go:369 msg="DumboDB server started" addr=127.0.0.1:27017 tlsMode=disabled data-dir=/tmp/backupdemo/primary
In another terminal, connect with mongosh:
$ mongosh mongodb://localhost:27017
Make Something Worth Saving#
A small shop database with three orders, committed:
test> db.getSiblingDB("shop").orders.insertMany([
{_id: 1, item: "widget", qty: 3},
{_id: 2, item: "gizmo", qty: 1},
{_id: 3, item: "sprocket", qty: 7}
])
{ acknowledged: true, insertedIds: { '0': 1, '1': 2, '2': 3 } }
test> db.getSiblingDB("shop").runCommand({dumboCommit: 1, message: "Opening stock"})
{
commitId: 'fqfuvgqhcf9e6g2i3tsaj6vn2gj3fpuk',
branch: 'main',
message: 'Opening stock',
author: 'dumbodb <dumbodb@dumbodb>',
timestamp: ISODate('2026-10-19T19:41:05.029Z'),
committer: 'dumbodb <dumbodb@dumbodb>',
committerTimestamp: ISODate('2026-10-19T19:41:05.029Z'),
ok: 1
}
Someone is planning a sale on a branch of their own:
test> db.getSiblingDB("shop").runCommand({dumboBranch: 1, action: "add", branch: "holiday-sale"})
{ branch: 'holiday-sale', ok: 1 }
test> db.getSiblingDB("shop@holiday-sale").orders.updateMany({}, {$set: {discount: 0.2}})
{
acknowledged: true,
insertedId: null,
matchedCount: 3,
modifiedCount: 3,
upsertedCount: 0
}
test> db.getSiblingDB("shop@holiday-sale").runCommand({dumboCommit: 1, message: "20% off everything"})
{
commitId: 'ro1l52a5kuofui8jboovrp5h517nine8',
branch: 'holiday-sale',
message: '20% off everything',
[... snip ...]
ok: 1
}
And back on main, a new order comes in that nobody has committed yet:
test> db.getSiblingDB("shop").orders.insertOne({_id: 4, item: "doohickey", qty: 2})
{ acknowledged: true, insertedId: 4 }
test> db.getSiblingDB("shop").runCommand({dumboStatus: 1})
{
branch: 'main',
dirty: true,
readonly: false,
changes: [
{
type: 'collection',
name: 'orders',
status: 'modified',
documents: { added: 1, removed: 0, modified: 0 },
indexes: { added: [], removed: [], modified: [] },
metadata: {}
}
],
ok: 1
}
That’s three kinds of state: committed history, a second branch, and a dirty working set. A backup that only kept the first one would not be much of a backup.
Back It Up#
Backups are managed with the dumboBackup command, which always runs against
the admin database and names the database it acts on with db. They require
a URL, which is identical to the URL you would provide for a remote you
would push to and pull from.
In our example, we’ll use a file:// URL, thus creating a local remote. You
can also use s3://, git+ssh://, or one of the other
supported formats for Dolt.
test> db.getSiblingDB("admin").runCommand({
dumboBackup: 1, action: "add",
db: "shop", name: "offsite", url: "file:///tmp/backupdemo/backups/shop"
})
{ name: 'offsite', url: 'file:///tmp/backupdemo/backups/shop', ok: 1 }
test> db.getSiblingDB("admin").runCommand({dumboBackup: 1, action: "list", db: "shop"})
{
backups: [ { name: 'offsite', url: 'file:///tmp/backupdemo/backups/shop' } ],
ok: 1
}
Adding a backup doesn’t copy anything. A sync does:
test> db.getSiblingDB("admin").runCommand({dumboBackup: 1, action: "sync", db: "shop", name: "offsite"})
{
url: 'file:///tmp/backupdemo/backups/shop',
root: '2aonpfsm2jfe6jviq8ttrrbb84rv48qv',
upToDate: false,
ok: 1
}
The root is the hash of the database’s entire state at the moment of the
sync. Every branch, tag and working set hangs off of it, which is why all of
them come along. On disk, the backup is just a Dolt storage directory:
$ ls /tmp/backupdemo/backups/shop
LOCK iltsl0cn7etvch3ak7tnkdu32sr6s11h.darc manifest oldgen
Sync again without changing anything, and there is nothing to do:
test> db.getSiblingDB("admin").runCommand({dumboBackup: 1, action: "sync", db: "shop", name: "offsite"})
{
url: 'file:///tmp/backupdemo/backups/shop',
root: '2aonpfsm2jfe6jviq8ttrrbb84rv48qv',
upToDate: true,
ok: 1
}
Make a change and sync once more. The root moves, and only the new chunks are copied:
test> db.getSiblingDB("shop").orders.updateOne({_id: 2}, {$set: {qty: 5}})
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
test> db.getSiblingDB("admin").runCommand({dumboBackup: 1, action: "sync", db: "shop", name: "offsite"})
{
url: 'file:///tmp/backupdemo/backups/shop',
root: '8cqe4mci4n75coap00bjlefoaj4th3m8',
upToDate: false,
ok: 1
}
The backup gained one small file next to the first, holding just what changed:
$ ls -l /tmp/backupdemo/backups/shop/*.darc
-rw------- 1 node node 1419 Oct 19 19:41 /tmp/backupdemo/backups/shop/81361grk3tflrjokhv9oabhogk68b7j8.darc
-rw------- 1 node node 4727 Oct 19 19:41 /tmp/backupdemo/backups/shop/iltsl0cn7etvch3ak7tnkdu32sr6s11h.darc
Note that neither the new order nor the qty change has been committed. They
are in the backup anyway.
Lose Everything#
Stop the server with Ctrl-C. Then, delete its data directory:
$ rm -rf /tmp/backupdemo/primary
That server has gone to the server afterlife. I’m sure it’s lovely there. Start a new one on a fresh, empty data directory:
$ dumbodb --data-dir /tmp/backupdemo/replacement
Reconnect with mongosh. There’s nothing here but admin:
test> db.adminCommand({listDatabases: 1, nameOnly: true})
{ databases: [ { name: 'admin' } ], ok: 1 }
Restore#
restore takes the backup’s url and the name of the database to create with
as:
test> db.getSiblingDB("admin").runCommand({
dumboBackup: 1, action: "restore",
url: "file:///tmp/backupdemo/backups/shop", as: "shop"
})
{
db: 'shop',
url: 'file:///tmp/backupdemo/backups/shop',
root: '8cqe4mci4n75coap00bjlefoaj4th3m8',
ok: 1
}
Same root as the last sync. All four orders are back, including the two changes nobody committed:
test> db.getSiblingDB("shop").orders.find().sort({_id: 1})
[
{ _id: 1, item: 'widget', qty: 3 },
{ _id: 2, item: 'gizmo', qty: 5 },
{ _id: 3, item: 'sprocket', qty: 7 },
{ _id: 4, item: 'doohickey', qty: 2 }
]
test> db.getSiblingDB("shop").runCommand({dumboStatus: 1})
{
branch: 'main',
dirty: true,
readonly: false,
changes: [
{
type: 'collection',
name: 'orders',
status: 'modified',
documents: { added: 1, removed: 0, modified: 1 },
indexes: { added: [], removed: [], modified: [] },
metadata: {}
}
],
ok: 1
}
They are still uncommitted, exactly as we left them. The history is intact:
test> db.getSiblingDB("shop").runCommand({dumboLog: 1})
{
commits: [
{
commitId: 'fqfuvgqhcf9e6g2i3tsaj6vn2gj3fpuk',
refs: [ 'HEAD', 'main' ],
parent1: 'i7sb6hllfnsqg75a8usanpbehb12mndm',
message: 'Opening stock',
timestamp: ISODate('2026-10-19T19:41:05.029Z'),
[... snip ...]
},
{
commitId: 'i7sb6hllfnsqg75a8usanpbehb12mndm',
message: 'Initialize database',
[... snip ...]
}
],
ok: 1
}
And so is the sale someone was planning:
test> db.getSiblingDB("shop").runCommand({dumboBranch: 1, action: "list"})
{
branches: [
{
name: 'holiday-sale',
commitId: 'ro1l52a5kuofui8jboovrp5h517nine8'
},
{
name: 'main',
commitId: 'fqfuvgqhcf9e6g2i3tsaj6vn2gj3fpuk',
current: true
}
],
ok: 1
}
test> db.getSiblingDB("shop@holiday-sale").orders.find().sort({_id: 1})
[
{ _id: 1, discount: 0.2, item: 'widget', qty: 3 },
{ _id: 2, discount: 0.2, item: 'gizmo', qty: 1 },
{ _id: 3, discount: 0.2, item: 'sprocket', qty: 7 }
]
Pick Up Where You Left Off#
One thing did not come back: the offsite backup definition itself. Like
remotes and users, backup definitions live in the admin database, and a
backup carries one database only:
test> db.getSiblingDB("admin").runCommand({dumboBackup: 1, action: "list", db: "shop"})
{ backups: [], ok: 1 }
Point the new server at the same backup, and the first sync has nothing to do. The backup already holds exactly what this server has:
test> db.getSiblingDB("admin").runCommand({
dumboBackup: 1, action: "add",
db: "shop", name: "offsite", url: "file:///tmp/backupdemo/backups/shop"
})
{ name: 'offsite', url: 'file:///tmp/backupdemo/backups/shop', ok: 1 }
test> db.getSiblingDB("admin").runCommand({dumboBackup: 1, action: "sync", db: "shop", name: "offsite"})
{
url: 'file:///tmp/backupdemo/backups/shop',
root: '8cqe4mci4n75coap00bjlefoaj4th3m8',
upToDate: true,
ok: 1
}
From here, syncs carry on incrementally, as if nothing had happened.
What’s Next#
As mentioned above, we have work to do to ensure that you can run a server
with --auth enabled, and also perform push/pull/backup operations. In general,
we haven’t extended the DumboDB RBAC system to include the version control
operations, or branch-level permissions. This is our focus until it’s great.
Tune in next week and I’m sure we’ll have more features to announce in that
space.
Join us on our Discord and tell us what you’re building!