Pre-1.0 hoardDB is pre-1.0. Expect breaking changes.

Backup and Restore

This page has not been rewritten for ring mode yet. Its examples use buckets with a type: and verbs that ring mode does not have, so read it as a description of the feature and not as commands that run. Getting started has the statements that run today, and Limitations lists what is not available yet.

hoarddb dump captures every database and bucket except blob payloads, which no dump contains — see Blobs are not restored at all and Store fields; both dump and restore print a warning naming each blob bucket and each blobStore field affected, on every run. Everything else is written to a directory of plain BSON files — the same shape standard dump tooling uses — and hoarddb restore reads it back into a running server. No second binary, no proprietary archive format: the .bson files this produces are ordinary BSON, readable by any BSON-aware tool, on the day you take the dump and after.

This page is the how-to drill, the guarantees the commands actually make, and the caveats worth knowing before you rely on this for something that matters. Every transcript below is copied from a real session: a server started with no configuration, real inserts, a real dump, real destruction of the data, and a real restore.

The Drill

Start a server the normal way (see Getting Started) and connect the CLI. Create a database with a hash bucket and a fifo bucket so the drill covers both an engine that dumps in native storage order and one where order is the data:

[h]oardDB> create database shop
OK  created database shop
Next: use shop
Then: create bucket <name> {type: hash | btree | fifo | lifo | heap | blob}   (hash is the default)
     create bucket Post {Photos lifoStore, Comments fifoStore}
     a store field is <name> <type>Store — hashStore, btreeStore, fifoStore, lifoStore, heapStore, blobStore
[h]oardDB> use shop
Switched to database 'shop'
[h]oardDB [shop]> create bucket Customers { type: hash, indices: ["Email"] }
OK  created bucket shop.Customers (type hash, whole)
Indexes: [Email]
[h]oardDB [shop]> db.Customers.insert({_id_: "c1", Name: "Ada Lovelace", Email: "ada@example.com"})
OK  id=c1
[h]oardDB [shop]> db.Customers.insert({_id_: "c2", Name: "Grace Hopper", Email: "grace@example.com"})
OK  id=c2
[h]oardDB [shop]> create bucket Orders { type: fifo }
OK  created bucket shop.Orders (type fifo)
[h]oardDB [shop]> db.Orders.push({item: "widget", qty: 3})
OK  id=668a4412-09e0-4f80-965e-91cbaa795914
[h]oardDB [shop]> db.Orders.push({item: "gadget", qty: 1})
OK  id=1e6d644a-5d95-43f2-beef-eeaf13fc76df
[h]oardDB [shop]> db.Orders.push({item: "gizmo", qty: 7})
OK  id=7f8c1e3b-80a1-4c18-a251-29be5e5811b0

dump — the standalone form

dump and restore are verbs of the same hoardDB binary, not a separate tool — dump and restore are also REPL statements sharing the same implementation (see below), because a database admin tool that splits “connect and query” from “back up and restore” into two programs is asking you to remember which binary does what:

hoarddb dump -address 127.0.0.1:17433 -user admin -password ****** -insecure --out ./dump
dumping shop/Customers...
  shop/Customers: 2 documents, 138 bytes, sha256:e6e192eaed90562ab4a7dbd1421902a85ab332bd7964fff001157ba6c8204d31
dumping shop/Orders...
  shop/Orders: 3 documents, 245 bytes, sha256:16fe3a1e5ca88e9030f70d4219762c07861175ef3e2c174cd881332d53aa5ad5
dump: 1 database(s), 2 bucket(s), 5 document(s), 383 byte(s)

-insecure is for a local development server, as in Getting Started — against anything else, pass the server’s certificate fingerprint instead.

dump, restore and ingest never stop to ask whether to trust a server they have not met: when it is not in ~/.hoarddb/known_servers they refuse, so a backup script cannot hang on a question nobody answers. Connect once with hoarddb and confirm the fingerprint to record the server, or pass -insecure against a development server. This is a change for dump, restore and ingest, which used to ask on a terminal. With no credentials to send they exit 1.

dump exits 1 when the command line or the --out directory is unusable, and 2 when the server refuses the scope, for example a database that does not exist. 0 is a clean dump, 3 a dump where some buckets failed, 4 an interrupted one.

The output directory:

dump/
  manifest.json
  shop/
    Customers.bson
    Customers.metadata.json
    Orders.bson
    Orders.dead.bson
    Orders.metadata.json

manifest.json:

{
  "format_version": 3,
  "hoarddb_version": "devel",
  "created_at": "2026-09-19T09:59:35Z",
  "source_node": "node1",
  "databases": ["shop"],
  "buckets": 2,
  "documents": 5,
  "bytes": 383,
  "blobs_included": false,
  "credentials_included": false,
  "globally_consistent": false,
  "incomplete": false
}

shop/Customers.metadata.json — one of these per bucket, carrying the store type, indexes, the replication factor the bucket declared (when it declared one) and a digest of the .bson file next to it:

{
  "format_version": 3,
  "hoarddb_version": "devel",
  "protocol_major": 1,
  "created_at": "2026-09-19T09:59:35Z",
  "source_node": "node1",
  "database": "shop",
  "bucket": "Customers",
  "store_type": "hash",
  "indexes": ["Email"],
  "index_specs": [{"id": 0, "name": "Email_1", "fields": ["Email"]}],
  "documents": 2,
  "bytes": 138,
  "digest": "sha256:e6e192eaed90562ab4a7dbd1421902a85ab332bd7964fff001157ba6c8204d31",
  "blobs_included": false,
  "replication_factor": 3,
  "placement": "whole"
}

A hash bucket’s placement (whole or spread) is recorded too, and restore creates the bucket with it, so a bucket with a unique index comes back whole and keeps the index. A dump written before the field has none, and its buckets restore as the create’s default, whole in this build. A restore into a bucket that already exists restores documents, not placement.

Every dump this build writes is format_version 3. restore accepts format 2 and format 3 dumps, and refuses any other version, naming the one it found and the ones it restores; a newer dump needs a newer build.

A queue or heap bucket can also have a <bucket>.dead.bson file, holding the bucket’s dead entries as records rather than live documents. Its count, byte size and digest are in metadata.json as dead, dead_bytes and dead_digest, and a restore writes those records back as dead entries, not as live documents. A bucket with no dead entry has no such file and none of those three members. A client older than format 3 refuses to dump a bucket with dead entries rather than write them into <bucket>.bson, where a restore would revive them; upgrade that client and dump again.

A dump also refuses a bucket that holds an index written by a newer version of hoardDB (after a rollback), naming the index: this version cannot describe it, so a dump would restore a unique index as a plain one or leave it out. Dump that bucket with the newer version instead.

shop/Customers.bson is a plain concatenation of BSON documents — no length prefix, no wrapper — which is what lets any BSON tooling read it unmodified. Note there is no users.json: see Account recreation is not implemented below.

The dump is plaintext — protecting it is your part

There is no password on these files and no --encrypt flag, deliberately: a dump is the artifact you carry to another machine, another version, another host, and sealing it under this server’s keys would make it restorable only where those keys exist, which is the opposite of a backup. That means the dump directory is exactly as sensitive as the database it came from — and a little more so, because it is one directory you can copy in a single command.

Protect it with tools you already trust: write it onto an encrypted volume, pipe the directory through gpg or age before it leaves the host, or give it the same permissions, retention and off-site policy as any other copy of the data. manifest.json holds no credentials (see Account recreation) but it does list every database and bucket, and the .bson files beside it are your documents, verbatim.

Destroy it

[h]oardDB [shop]> drop bucket Customers
OK  bucket "Customers" dropped from "shop" (data removed from disk)
[h]oardDB [shop]> drop bucket Orders
OK  bucket "Orders" dropped from "shop" (data removed from disk)
[h]oardDB [shop]> show buckets
BUCKETS in shop
---
(no buckets)

restore

hoarddb restore -address 127.0.0.1:17433 -user admin -password ****** -insecure --dir ./dump
restoring shop/Customers (2 document(s))...
  shop/Customers: 2 applied, 0 skipped
restoring shop/Orders (3 document(s))...
  shop/Orders: 3 applied, 0 skipped
restore: 1 database(s), 2 bucket(s), 5 document(s) applied, 0 skipped

While a node joins the cluster, a restore is refused with retry. Starting a bucket’s restore creates or drops it, and schema changes pause until the join settles. A restore already under way finishes the bucket it is on; the next bucket’s start is refused, and the summary names the buckets that landed and the one refused, with the server’s reason:

restore: 1 database(s), 1 bucket(s), 2 document(s) applied, 0 skipped (incomplete: 1 bucket(s) failed)
  restored shop/Customers
  FAILED shop/Orders: Schema changes are paused while n4 joins the cluster ... Run this again once 'nodes' shows the join settled - it settles on its own. ...

Run the restore again once nodes shows the join settled. A restore --dry-run is answered during a join, and adds that a real restore would be refused now.

Verify — counts and contents

[h]oardDB [shop]> show buckets
NAME       TYPE  ENTRIES
Customers  hash  2
Orders     fifo  3
[h]oardDB [shop]> db.Customers.get("c1")
{"Email": "ada@example.com", "Name": "Ada Lovelace", "_id_": "c1"}
[h]oardDB [shop]> db.Customers.get("c2")
{"Email": "grace@example.com", "Name": "Grace Hopper", "_id_": "c2"}
[h]oardDB [shop]> db.Orders.length()
3
[h]oardDB [shop]> db.Orders.pop()
{"_id_": "668a4412-09e0-4f80-965e-91cbaa795914", "item": "widget", "qty": 3}
[h]oardDB [shop]> db.Orders.pop()
{"_id_": "1e6d644a-5d95-43f2-beef-eeaf13fc76df", "item": "gadget", "qty": 1}
[h]oardDB [shop]> db.Orders.pop()
{"_id_": "7f8c1e3b-80a1-4c18-a251-29be5e5811b0", "item": "gizmo", "qty": 7}

Values round-tripped exactly, and the queue popped back out in the same order it was pushed in (widget, gadget, gizmo) — order is the data for a FIFO bucket, and the dump preserves it.

The REPL form

The exact same grammar works as a statement inside an interactive session, so you can dump one bucket without leaving the shell. Flags come before the positional database bucket arguments, same as the standalone form:

[h]oardDB [shop]> dump --out ./dump-repl shop Customers
dumping shop/Customers...
  shop/Customers: 2 documents, 138 bytes, sha256:4892e5983f3f67eb93462c22a2cf2c9de4223f83cc0fb02af93677f4e4273920
dump: 1 database(s), 1 bucket(s), 2 document(s), 138 byte(s)

Scripting it

--json prints exactly one JSON object to stdout — progress, byte counts and rate go to stderr — so a pipeline can parse the result without the human-readable noise mixed in:

hoarddb restore -address 127.0.0.1:17433 -user admin -password ****** -insecure \
  --dir ./dump --drop --json 2>restore.log
{"ok":true,"databases":["shop"],"buckets":1,"documents_applied":2,"documents_skipped":0,"elements_skipped":0,"users_recreated":0,"users_awaiting_password":0,"warnings":[],"errors":[]}

Exit codes: 0 everything was restored, 1 usage error (a bad flag, or a --dir that is missing, unreadable or not a dump), 2 refused before anything was applied (the server declined the restore), 3 partial (a bucket failed — check errors — or data was skipped: documents_skipped or elements_skipped above zero, which is what a blob bucket or a blobStore field gives you), 4 interrupted. A script that restores a database with blob buckets or blob fields should expect 3, not 0, and read the two skipped counts to see what was left behind. restore --drop replaces each target bucket instead of merging into it; without --drop, restore merges — and whether a re-run is safe depends on the bucket’s store type, because the underlying write is different per engine:

  • hash and btree restore through an upsert (OpUpsert), which replaces the document stored under a key and adds it when the key is new. Re-running the same restore twice converges: the second run replaces the same keys rather than adding to them.
  • FIFO and LIFO restore through a push (OpFIFOPush, or a reverse-push for LIFO — see server/restore.go’s applyRestoreDocument). A dump lists a LIFO bucket newest first, so its restore pushes nothing until the last element has arrived: the elements wait in a spill file in the data directory, sealed under a key held only in memory and removed when the restore ends, and are pushed oldest first when the restore finishes. The node needs free disk for that file, about the dump’s size, beyond the usual headroom; memory stays small whatever the bucket’s size. A queue element has no key to converge on, so re-running the same restore against one of these buckets delivers every element again, on top of what the first run already added. This is the specified, intentional behavior for a push-based engine, not a bug — but it means a cron job or retry loop that blindly re-runs restore must not merge into a FIFO/LIFO bucket that already received a previous run; use --drop to replace the bucket outright instead of merging into it a second time.
  • A heap restores through a push too (OpHeapPush, see server/restore.go’s applyRestoreHeapDocument), but it converges like a keyed engine: a restored element keeps the _id_ it was dumped with, and a push under an _id_ already present at the same priority overwrites it. Re-running the same restore twice leaves the heap at the same count. The one caveat is an element whose priority moved between the two runs: the entry key is the id and the priority, so the restored copy lands beside the element’s current self rather than on top of it.
  • Blob buckets restore nothing at all, on the first run or any subsequent one — see Blobs are not restored at all below.

hoarddb dump --out /backups/$(date +%F) is the whole cron line: it needs no prompt, no TTY, and no flag to confirm when the destination is a fresh directory.

What the dump promises about consistency

Per bucket, a dump is a consistent point in time. Across buckets, it is not. This is how a logical dump normally behaves, for the same reason, and it is worth stating as plainly: a dump of shop reads Customers as of one instant and Orders as of a slightly later instant, because each bucket is read independently while the rest of the server keeps serving writes. manifest.json carries "globally_consistent": false so nothing downstream has to guess. If you need every bucket frozen at exactly the same instant, that is not what a logical dump gives you — stop writes for the duration, or dump during a quiet window.

Within one bucket, though, the guarantee is real: a hash/btree/heap bucket’s dump is read from a single consistent snapshot, and writers are not blocked while it runs. A FIFO/LIFO bucket’s dump is the queue as of the moment it started capturing, and the queue keeps taking pushes and pops throughout — see the next section for what that means for elements popped mid-dump.

One dump covers the whole cluster

On a multi-node cluster, a dump with no database/bucket argument backs up every bucket in the cluster, whichever member you connect to. The bucket listing is answered for the whole cluster, not for the node you dialled, and each bucket is then read from the node that owns it — so a bucket held by several replicas is dumped once, from its owner, not once per copy. There is no need to run dump against each node, and no reason to: every such dump would hold the same buckets.

A scoped dump behaves the same way: hoarddb dump shop Orders reaches Orders even when the node you are connected to is not the one that holds it.

The one thing that stops a scopeless dump is a member that is down or silent. Producing the complete bucket list needs every member’s answer, and a short list would read as a complete backup, so the dump is refused with a message naming the member that did not answer and asking you to retry — run nodes to see member state. That refusal is deliberate: a dump that quietly left out one node’s buckets is the one failure a backup cannot have. A dump that names a database and bucket does not need the cluster listing, so it does not care which members are down as such. What it needs is a node that holds the bucket: the node you are connected to answers it when that node holds a copy, and otherwise the request goes to the bucket’s owner. It fails, naming the owner, only when the owner is the member that is down and the node you are connected to holds no copy of the bucket. At replication factor 1 each bucket has one holder, so it fails whenever that holder is down; with more copies, connect to a member that holds one. For a bucket recorded spread, the holder the request must reach is its router; see Spread buckets.

Restore is unaffected by any of this: it always goes through a single connection to a single server (-address names it), the same as any other write.

Spread buckets

A bucket recorded spread is dumped as one bucket, under its own name. Its router asks every part, and the dump streams their documents into one file; no part’s name appears in the dump, the manifest or an error. metadata.json records "placement": "spread", so restore creates the bucket with that placement and writes each document back to its part, through the bucket’s router.

A spread bucket’s dump needs its router, the node that owns the bucket’s name, because every document is read through it. A scoped dump therefore works from any node you can reach while the router answers, and cannot run while the router is down, even if the node you dialled holds a copy of the bucket’s name. A part owner that does not answer refuses the bucket’s dump; the dump reports that bucket as failed and exits 3, marking "incomplete": true in manifest.json. It never writes part of a spread bucket as a complete one.

A second dump running at the same time is refused when it reaches a part the first dump is reading, naming the first bucket: a spread dump holds the dump slot of each part only while it reads that part, not one slot on the router. Wait for the first dump to finish, or dump another collection. A second dump of the same collection while the first runs is refused when it starts, with the “a dump is already running” refusal that names the first dump.

Queues: what you get back may be a superset

Dumping a FIFO or LIFO bucket does not pause it. Pushes during the dump simply land after the captured range and are not included — nothing surprising there. Pops are the case worth knowing: a pop advances the queue’s head, but the bytes the dump already captured stay readable until the dump finishes with that bucket, so an element popped during the dump can still end up in the dump file.

The practical effect: a restore can re-deliver an element your application already consumed. For an at-least-once consumer this is exactly the cost of being at-least-once and is harmless. For an exactly-once consumer it is data — and this release does not tell you when it happened: the design this feature was specified against calls for a "concurrent_mutations_detected" manifest flag, but no such field is written by dump or read anywhere in this release (verified: cli/dump.go’s topLevelManifest has no such field, and neither concurrent_mutation nor ConcurrentMutations appears anywhere in the codebase outside the specification document). There is currently no flag to check. If you dump a FIFO or LIFO bucket that is being actively popped from, assume the restored queue may contain elements your application already processed — there is no way to confirm this after the fact from the dump’s own output. The way to get an exact, no-redelivery snapshot of a queue today is to dump it from a period with no concurrent pops.

Elements that were deleted (tombstoned) before the dump captured them are never emitted — those do not come back. LIFO is dumped in pop order (newest-first), so a restore reproduces the stack correctly rather than reversing it.

The compaction-pin ceiling on FIFO/LIFO dumps

Dumping a queue holds a pin that blocks that bucket’s background compaction for as long as the dump is reading it — pushes and pops are unaffected, but disk space used by consumed entries cannot be reclaimed until the pin releases. The pin is released the moment the dump finishes the bucket, the connection closes, or a ceiling is hit — whichever comes first.

The ceiling defaults to 5 minutes and is configurable:

SettingDefaultMeaning
HOARDDB_DUMP_PIN_CEILING_SECONDS (YAML: server.dump_pin_ceiling_seconds)300Maximum time a queue dump may hold compaction pinned before the dump fails loudly with a named error, rather than pinning an unbounded amount of time on a busy queue

A FIFO or LIFO bucket under continuous, heavy pop traffic can outrun this ceiling before its dump finishes. If that happens to you, dump that bucket during a maintenance window, or raise the ceiling for the duration of the dump and lower it again afterward — a pin held indefinitely against a high-throughput queue is a disk-exhaustion risk, which is exactly what the default keeps bounded.

The 16 MiB portability caveat

Most third-party BSON tooling assumes every document is at most 16 MiB — a convention, not a limit of the format. hoardDB’s own limits are different: a FIFO/LIFO entry may be up to the segment size (64 MiB by default), so a .bson file this tool produces from a legitimate bucket can exceed that 16 MiB convention. That is not a bug in the dump — it is a faithful copy of data that was already legally in the bucket — but it means a .bson file from a queue-heavy hoardDB installation is not guaranteed to open in tooling that hard-codes the 16 MiB assumption. hoardDB’s own restore has no such limit; the caveat is specifically about handing the file to something else.

Oversized documents are refused, not truncated

There is a real ceiling on what dump will write for you, and it is lower than the 64 MiB segment size above: ~31.94 MiB (33,488,896 bytes exactly — the 32 MiB wire-frame limit, minus headroom reserved for framing overhead). A document that cannot fit inside one dump chunk is refused outright: dump names the bucket and the key (or “queue element” for a FIFO/LIFO entry with no key) and points at blob buckets as the place for a payload that large. The bucket’s dump fails; the rest of the run continues, and the failure is recorded in manifest.json.

If you store payloads anywhere near this size, use a blob bucket instead of hash/btree/heap/fifo/lifo — blob payloads are designed for large binary content, and no dump chunk ceiling applies to them.

That trade is not free, and it is the opposite of a backup guarantee: a payload moved into a blob bucket leaves the dump entirely. A document that is too large for dump fails loudly and is recorded in manifest.json; the same bytes in a blob bucket are simply not captured, and restore brings the bucket back empty (next section). Until that changes, back blob payloads up out of band — dump and restore both warn on every run and name each blob bucket, so a scripted backup can fail on the warning rather than discover it during a recovery.

Blobs are not restored at all

Blob payloads are excluded from every dump, deliberately — manifest.json and each blob bucket’s metadata.json carry "blobs_included": false. The dump side does capture each blob’s metadata (key, filename, content type, size, checksum, creation time) into that bucket’s .bson file — a dump of a blob bucket is not empty on disk, and this is a deliberate default, not a current limitation: a dump that silently balloons into a full copy of every payload you store is the kind of thing that gets run once and never trusted again.

restore, however, does not write any of that back. In this release, every document in a blob bucket’s dump — metadata included — is counted into the restore’s skipped total and otherwise discarded (verified: server/restore.go’s applyRestoreDocument, the BucketTypeBlob case, increments a skip counter and returns with no write of any kind). A restored blob bucket in this release ends up completely empty — not “metadata present, payload missing.” restore does report the count of blob documents it skipped, so this is not silent, but do not expect a restored blob bucket to hold anything: no key, no filename, no size, no timestamp comes back. If you need a blob bucket’s metadata after a restore, read it from the dump directory’s .bson/.metadata.json files directly — dump captured it — rather than from the restored bucket, which restore does not populate.

Store fields

A bucket can declare store fields, so each document owns a list, a map, a queue or a blob store of its own (Comments fifoStore, Photos blobStore). A dump records them in two extra files per bucket, <bucket>.stores.bson for the elements and the stores list in <bucket>.metadata.json for the declaration, and restore writes them back.

Field engineWhat restore brings back
hashStore, btreeStoreevery element, by its key
fifoStore, lifoStore, heapStoreevery element, in the same pop order (positions are assigned afresh)
blobStorenothing: the field is declared again and is empty

A blobStore field’s elements are not restored for the same reason a blob bucket’s are not: the dump carries no payload. Nothing about this is silent. Both dump and restore warn, before anything is written, and name the bucket and field (app/Post (blobStore field Photos)); the closing summary counts the elements left behind, and --json carries the count as elements_skipped and the warning in warnings. The exit code is 3, and ok is false, because data was left behind:

restore: 1 database(s), 1 bucket(s), 1 document(s) applied, 0 skipped, 2 store-field element(s) skipped
{"ok":false,"databases":["app"],"buckets":1,"documents_applied":1,"documents_skipped":0,"elements_skipped":2,"users_recreated":0,"users_awaiting_password":0,"warnings":["blob payloads are not captured by any hoardDB dump yet: 1 blob bucket(s) in this dump have no payload bytes to restore — app/Post (blobStore field Photos). ..."],"errors":[]}

restore --drop refuses to run against a bucket that declares a blobStore field, and against a dump that declares one, because dropping the bucket would destroy elements the restore cannot put back. The bucket and its elements are left as they were:

FAILED app/Post: restore start: server error (5): restore --drop would destroy blob payloads dump never captured: the bucket declares the blobStore field(s) Photos of app/Post — dump never captures blob field elements, so --drop would permanently destroy them recreating this bucket; run 'drop bucket Post' yourself first if this is really what you want, or restore into a different bucket

Copy the blob field’s data out of band before you rely on the backup, as you would for a blob bucket. If you really do want the bucket replaced, run drop bucket Post yourself and then restore.

A bucket’s replication factor

A bucket created with { replication_factor: N } keeps that factor through a dump and a restore. The dump writes "replication_factor": N into the bucket’s metadata.json; a bucket that declared no factor, and inherits the node’s, has no such field, and a restore creates it inheriting the target’s default. A dump taken before dumps recorded the factor reads the same way.

To see it, start the three-node local cluster from Replication (client ports 7433 to 7435), create a bucket at factor 2, dump it, and read the field back:

printf 'create database shop;\nuse shop\ncreate bucket Orders {type: hash, replication_factor: 2};\ndb.Orders.insert({_id_: "o1", total: 12})\nexit\n' | \
  ./hoarddb --insecure --user admin --password "$HOARDDB_ROOT_PASSWORD" --address localhost:7433
./hoarddb dump --insecure --user admin --password "$HOARDDB_ROOT_PASSWORD" --address localhost:7433 \
  --out ./dump-rf shop Orders
grep replication_factor ./dump-rf/shop/Orders.metadata.json

Restore it over the same bucket with --drop, and it is re-created at factor 2; restore it into a standalone server (one hoarddb-server started on its own, here on 17433), and the summary ends with the block shown in the first case below, because one server keeps one copy:

./hoarddb restore --insecure --user admin --password "$HOARDDB_ROOT_PASSWORD" --address localhost:7433 \
  --drop --dir ./dump-rf
./hoarddb restore --insecure --user admin --password "$HOARDDB_ROOT_PASSWORD" --address localhost:17433 \
  --dir ./dump-rf

restore creates each bucket with the factor its dump records, both when the bucket is new and when --drop re-creates it. A bucket that does not end up with that factor is never silent: the summary ends with one block per case, on stderr, naming at most ten buckets and then “and N more”, and --json lists every one as database/bucket in factor_not_restored, factor_kept or factor_lowered (each present only when it has a bucket in it). None of them changes the exit code: the documents were restored.

  • The target cannot grant the factor. A standalone server keeps one copy, and a cluster cannot place more copies than it has members. The bucket is created inheriting the node default, and the data is restored:

    restore: 2 bucket(s) were restored WITHOUT the replication factor their dump records
      shop.Events (3), shop.Orders (3)
      This server cannot place that many copies (it is standalone, or has fewer members than the factor), so they use the node default. Add members ('join' on each new node, then 'nodes' to count them), then run this restore again with --drop.
    
  • The bucket already exists and the restore merges into it. A merge restores documents, not placement, so the bucket keeps its own factor (shop.Orders (kept 1; the dump records 3)). Run the restore with --drop to take the dump’s factor.

  • --drop over a bucket that declared a factor, from a dump that records none. The bucket is re-created inheriting the node default, and the summary says its factor was LOWERED (shop.Audit (was 3; the dump records none)). To keep the factor, drop the bucket, create it with { replication_factor: 3 }, and restore without --drop.

restore --dry-run reports the same three cases without creating anything, in the conditional: restore --dry-run: 2 bucket(s) would be restored without the replication factor their dump records, would keep their own replication factor, would have their replication factor LOWERED by --drop.

Account recreation is not implemented in this release

hoardDB dumps and restores data and bucket definitions. It does not dump or restore user accounts.

Concretely: dump never writes a users.json file, and restore never reads one. If a dump directory happens to contain a users.json — hand- written, copied in from a different tool, or left over from a future version — restore notices it, prints a warning, and does not act on it:

warning: users.json present but account restore is not yet implemented; accounts were not recreated
{"warnings":["users.json present but account restore is not yet implemented; accounts were not recreated"]}

The warning appears in both the text summary and --json output, never only as a log line, so a script that only checks the exit code still has somewhere to see it if it parses warnings.

This is a real limitation of this release: user accounts are managed on the server (see User management), but dump does not capture them yet and restore does not recreate them. A future release that adds persistent, mutable user accounts is expected to add account recreation to dump/restore at the same time; until then, back up your credential configuration (HOARDDB_ROOT_USER/HOARDDB_ROOT_PASSWORD or your config file) the way you already do for any other secret.

Upgrading the storage layout

This build keeps every bucket of a node in one shared store. Earlier builds kept each bucket in a directory of its own. The server does not convert a data directory from one layout to the other; it refuses to start on the old one and says so:

Data directory ./data uses the per-bucket storage layout (catalog version 1).
This server writes the shared layout (version 2) and will not convert data in place.
Back up with the previous release's 'hoarddb dump', then restore into a fresh
directory with this release — see docs/user/backup-restore.md#upgrading-the-storage-layout.

The refusal comes from what is in the directory, not from its catalog alone. A data directory that has a catalog.json is refused, and so is one with no catalog.json that still holds bucket directories (<database>/<bucket>/ with a badger/, segments/, blobs/ or indexes/ directory, or a key.wrapped file, inside). The check is the first thing the server does, before it writes a key, a token or a store, and it only lists directory names, so the old directory is exactly as you left it.

To move to the shared layout, dump with the build that wrote the data and restore into a new, empty data directory with this one:

  1. Start the earlier build on the old data directory and take a dump, as in The Drill:

    hoarddb dump -address 127.0.0.1:17433 -user admin -password ****** -insecure --out ./dump
    
  2. Stop it. Start this build on a new data directory (a fresh ./data, or another path given with the data directory setting). Do not point it at the old one.

  3. Restore into the new server:

    hoarddb restore -address 127.0.0.1:17433 -user admin -password ****** -insecure --dir ./dump
    
  4. Check the counts with show buckets and db.<bucket>.count() before you delete the old directory. Keep the old directory and the dump until you have.

A cluster has one data directory per node, and each is refused the same way. Take one dump of the cluster before you stop any of the nodes (see One dump covers the whole cluster); it holds every bucket whichever member you connect to, each bucket once, from its owner. Then start this build on a fresh data directory for each node and restore through one node. Restore one bucket once: running the same restore into a queue bucket twice would deliver its entries twice.

What a dump carries is what it carries on any build, and the sections above still apply:

  • Hash, btree, fifo, lifo and heap buckets come back with their documents and index declarations. A queue comes back in order.
  • Blob buckets do not. Blob payloads are not in a dump and restore writes nothing into a blob bucket (see Blobs are not restored at all). A data directory with blob buckets cannot be upgraded with a dump alone: copy the files out of the old directory yourself first, and upload them again after the restore.
  • Users are not in a dump (see Account recreation is not implemented in this release). The new server starts with its own root credential.
  • A dump is plaintext, even from an encrypted node (see The dump is plaintext). Delete it when the restore is verified. Whether the new node encrypts its data is decided by its own configuration when its directory is first started; see Configuration.

When a store file is damaged

A node keeps its buckets in table files under <data dir>/store, and every block of those files carries a checksum that is checked when the block is read from disk. A block that fails it — a bad disk, a torn copy — is never answered as data and never as “not found”. A read of a document in it fails:

bucket "users" in database "app" could not be read: a file of the node store
at /var/lib/hoardDB/store is damaged, so the read failed and returned nothing
(a table block could not be read: table /var/lib/hoardDB/store/000042.sst: ...
checksum mismatch). Reads of the damaged part keep failing rather than answer
wrongly. Stop this node and move /var/lib/hoardDB/store aside ...

A search, a dump or a page that walks across the block fails the same way instead of leaving the block’s documents out, so a dump of a damaged node is refused rather than taken incomplete. The rest of the node keeps answering.

The node logs the first such read at ERROR and counts every one in the Prometheus counter hoarddb_store_damaged_reads_total. Alert on any increase: the damage does not heal, and a node with damaged files is one disk failure away from losing data its replicas do not hold.

To repair it, replace the node’s store:

  1. If the node’s buckets have replicas on other nodes, remove the node with remove-node <node-id>, stop it, move its data directory aside (keep it until the node is back: its undamaged files may hold the only copy of something), start it with an empty data directory and join it again. It is seeded from the other holders. See Replication.
  2. Otherwise restore the node from your last dump, as in The Drill, into a fresh data directory. Writes since that dump are lost.

Summary of flags

FlagApplies toMeaning
--out <dir>dumpDestination directory (default ./dump). Refused if non-empty and not itself a previous dump
--dir <dir>restoreSource directory (default ./dump)
--droprestoreDrop each target bucket before restoring it. Without it, restore merges — see the per-engine re-run behavior above before relying on merge for FIFO/LIFO (a heap merges safely)
--allow-incompleterestoreRestore from a dump manifest marked "incomplete": true (one or more buckets failed to dump). Refused by default so an automated pipeline does not silently apply a partial backup
--dry-runrestoreValidate every file and report what would change; write nothing
--jsonbothOne JSON result object on stdout; progress on stderr
-address, -user, -password, -insecurebothThe same connection flags the CLI itself takes

dump [database [bucket]] and restore (source is always --dir) both work as REPL statements with the same flags, minus the connection ones — the REPL is already connected.

Source: docs/user/backup-restore.md in the repository.