The .qbak archive format
- PostgreSQL
- MySQL
- MariaDB
- MongoDB
- Redis / Valkey
- CLI
A .qbak file is what Querybara’s backups write by default: the desktop app’s Backup wizard, its
scheduled backups and querybara backup all produce it. It holds one entry per object (DDL, rows, documents or keys) and
a manifest that describes them, so a restore can pick objects without reading the rest. An archive
can be encrypted with a passphrase; then every byte after the header is authenticated, and any
change to the file is detected.
This page describes version 1 of the format. It is the contract: a reader written from it opens
every archive Querybara writes. The reference implementation is in packages/backup/src/archive/ of
the Querybara source.
Layout
Section titled “Layout”All integers are unsigned and big-endian. A file is a header, the entries, the manifest and a trailer:
header magic | u16 version | u16 flags | u32 n | n bytes of JSON | [32-byte MAC]entry 0 entry header | frame | frame | ... (the last frame has the LAST bit)entry 1 ...manifest an entry with index 0xFFFFFFFF, holding the manifest JSONtrailer "QBAKEND\0" | u64 manifest offset | u64 manifest length | u32 0 | u32 CRC-32Header
Section titled “Header”| Bytes | Field |
|---|---|
| 0–7 | Magic 51 42 41 4B 0D 0A 1A 0A (QBAK\r\n, Ctrl-Z, \n) |
| 8–9 | Format version: 1 |
| 10–11 | Flags: 0x1 encrypted, 0x2 compressed; every other bit is 0 |
| 12–15 | n, the length of the header JSON (at most 64 KiB) |
| 16– | The header JSON, UTF-8 |
| then | Encrypted archives only: HMAC-SHA256 of bytes 0 to the end of the JSON |
The header JSON is {"compression": "gzip" | "none"}. An encrypted archive adds
"cipher": "aes-256-gcm" and "kdf": {"name": "scrypt", "log2N", "r", "p", "salt"}, where the
salt is 16 random bytes in base64. The flags and the JSON must agree.
A reader refuses other versions and other ciphers. It also refuses scrypt costs outside log2N
10–20, r 1–32, p 1–16 and 1 GiB of memory, so a crafted header cannot make it spend hours or
gigabytes.
Entries and frames
Section titled “Entries and frames”An entry is a 16-byte entry header followed by frames:
| Bytes | Field |
|---|---|
| 0–3 | 51 45 4E 54 (QENT) |
| 4–7 | The entry’s index: 0, 1, 2… in file order; 0xFFFFFFFF is the manifest |
| 8–15 | Nonce prefix: 8 random bytes when encrypted, zeros when plain |
Each frame starts with a u32 word: bit 31 is set on the entry’s last frame, and bits 0–30 hold
the payload length. Then:
- in a plain archive, the payload, then a CRC-32 of the word and the payload;
- in an encrypted archive, the AES-256-GCM ciphertext of the payload (the same length), then its 16-byte tag.
Writers use 64 KiB payloads; readers refuse frames over 16 MiB. Every entry has at least one frame: an empty entry is one empty last frame.
Joined together, the payloads are the entry’s stored content. With the compressed flag, that is a gzip stream of the content, each entry compressed on its own; otherwise it is the content itself.
Encryption
Section titled “Encryption”The passphrase, normalised to Unicode NFC, and the header salt go through scrypt with the header’s
cost, giving 64 bytes. The first 32 are the AES-256-GCM key, the last 32 the HMAC key. The header
MAC tells a wrong passphrase apart from a damaged file before any entry is read. Querybara writes
log2N 17, r 8, p 1. The passphrase is not stored anywhere.
Frames follow the STREAM construction. Frame i, counting from 0 in each entry, is sealed with:
- nonce: the entry’s 8-byte prefix followed by
u32 i(12 bytes); - associated data: the 16-byte entry header,
u32 i, and one byte:1on the last frame,0otherwise.
A frame therefore cannot be changed, reordered, moved to another entry or index, or dropped from the end of an entry without the tag check failing. Nonce prefixes are random per entry, so nonces never repeat under one key.
Manifest and trailer
Section titled “Manifest and trailer”The last 32 bytes of the file are the trailer. Its CRC-32 covers its first 28 bytes. The manifest offset plus the manifest length plus 32 must equal the file size.
The manifest entry at that offset is framed, compressed and encrypted like the other entries, and holds UTF-8 JSON:
{ "format": "querybara-backup", "formatVersion": 1, "createdAt": "2026-09-29T12:00:00.000Z", "producer": "Querybara", "engine": "postgres", "serverVersion": "16.4", "database": "shop", "databaseOptions": { "encoding": "UTF8" }, "options": { "snapshot": "repeatable-read", "compression": "gzip", "encrypted": true }, "objects": [ { "id": "table:public.orders:create", "kind": "table", "schema": "public", "name": "orders", "qualifiedName": "public.orders", "dependsOn": ["schema:public:create"], "ddl": "ddl/0003-table-public.orders.json", "data": { "entry": "data/0003-public.orders.sql", "count": 1200, "columns": ["id", "total"] } } ], "entries": [ { "name": "ddl/0003-table-public.orders.json", "index": 4, "offset": 5120, "storedLength": 311, "size": 402, "sha256": "…64 hex digits…", "contentType": "application/json" } ], "warnings": []}Objects
Section titled “Objects”objects are in creation order. kind is one of schema, extension, type, sequence,
table, partition, index, unique, check, primary-key, column, foreign-key,
trigger, view, materialized-view, routine, event, grants, collection or keys.
dependsOn lists the ids that must exist first. parent names the table that an attached object,
such as a foreign key or a trigger, belongs to. A selective restore takes the chosen objects, what
they depend on, and what is attached to them; a foreign key comes only with both of its tables.
Entries
Section titled “Entries”entries locate every entry except the manifest:
offsetis the position of the entry header;storedLengthis the length of the entry header and its frames;sizeandsha256(lower-case hex) describe the uncompressed content. A reader checks both after reading an entry.
contentType is application/sql, application/json, application/bson or
application/x-ndjson.
What the entries hold
Section titled “What the entries hold”PostgreSQL, MySQL and MariaDB
Section titled “PostgreSQL, MySQL and MariaDB”ddl/NNNN-<kind>-<name>.json holds one object’s statements:
{"pre": [...], "data": [...], "post": [...], "grants": [...]}.
precreates the object.dataruns after the rows, for example sequence positions and materialized view refreshes.postruns after every table is loaded: foreign keys, triggers and events.grantsis optional.
data/NNNN-<name>.sql holds a table’s rows as multi-row INSERT statements, each ending with ;
and a newline.
A restore runs every pre, then the rows, then every data, then post, then grants.
MongoDB
Section titled “MongoDB”meta/NNNN-<name>.json is {"name", "type", "options", "indexes"}, with the listCollections
options (validator, collation, capped, time series and so on) and each index specification as
canonical Extended JSON.
data/NNNN-<name>.bson holds concatenated BSON documents. With Extended JSON chosen instead, the
entry is .jsonl, one canonical Extended JSON document per line.
keys/<database>.bson holds concatenated BSON documents, one per key:
| Field | Value |
|---|---|
k |
The key, binary |
v |
The DUMP payload, binary |
t |
int64 PTTL in milliseconds, -1 for none |
x |
int64 expiry in Unix milliseconds, or null |
A restore uses RESTORE with the remaining TTL, or with ABSTTL and x when asked to keep the
original expiry (querybara restore --keep-expiry).
Compatibility
Section titled “Compatibility”A version 1 reader refuses archives with another version in the header or the manifest. New
optional manifest fields, new detail and options values, and new header JSON fields can be
added without a version change; readers ignore what they do not know. Anything that changes the
byte layout, the cipher construction, the list of object kinds or the meaning of an existing field
needs version 2.
Related
Section titled “Related”Documents Querybara 0.1.1 · built frombc9f5aa