Skip to content

Export bundle format

A Marrow export bundle is a zip file with a transparent, human-readable structure. You can unzip it and read it without any Marrow tooling.

For why this format exists and how the guarantee is enforced, see Restore guarantee.

Bundle schema v4 is the current format. This table matches the restore guarantee scope:

CategoryIncluded in bundleNotes
Exported today (v4)Node tree, revisions, attachments, node_properties, node_links (links.json), trash (include_trash)Round-trips via marrow restore; CI verifies with test_round_trip.py
Never exportedStars, Inbox notifications, node watchesUser-scoped tables — not workspace content
Planned v5Comments, share links, folder view definitionsNot in v4 bundles yet; restore guarantee will extend when bundle v5 ships

Search indices and other derived state are not in the bundle — they are rebuilt on restore.

marrow-export-{workspace-slug}-{timestamp}.zip # full
marrow-export-{workspace-slug}-slim-{timestamp}.zip # slim

The marrow restore CLI also accepts the legacy freehold-export-* prefix from bundles produced before the project rename.

bundle.zip
├── manifest.json
├── nodes/
│ ├── {node-id}.md
│ └── {node-id}.json
├── revisions/
│ └── {node-id}/
│ ├── {revision-id}.md
│ └── {revision-id}.json
├── assets/
│ └── {attachment-id}{ext}
└── links.json

Folder nodes appear only in manifest.json — they have no files under nodes/. Only page-typed nodes get content files.

Contains workspace and org metadata, all entity IDs, and the bundle schema version. Schema is currently v4 (Marrow 0.2+). Restore supports v1, v2, v3, and v4: older bundles are auto-upgraded — their legacy collection/page structure is mapped onto the new nodes tree on read.

v4 manifests carry:

  • The full node tree (folders + pages), each node’s parent_id, position (fractional index), and deleted_at
  • node_properties — folder property schemas and page values
  • include_trash — whether soft-deleted nodes were included in the export

Current state of every page-typed node.

  • {node-id}.md — Markdown render of the current revision (always present).
  • {node-id}.json — canonical BlockNote JSON (present when the current revision is JSON-format).

The Markdown is for humans. The JSON is what gets restored byte-for-byte.

v3 bundles used a pages/ directory with the same file naming; v4 renamed it to nodes/ to match the unified node tree.

The full append-only history. Each page-typed node has a subfolder containing every revision. Same .md + .json convention as nodes/.

Slim bundles omit this directory entirely. The manifest sets "slim": true and "revisions": []. Restore recreates a single revision per page from the nodes/ content.

CLI: marrow export --slim. API: ?slim=true.

By default, exports omit nodes that have a deleted_at set, so a backup matches what users currently see. To include trash:

  • CLI: marrow export --include-trash
  • API: ?include_trash=true

The manifest records the choice ("include_trash": true|false). Restore replays each included node’s deleted_at so trash is preserved across the round trip.

Every attachment, named by attachment ID with the original extension.

Internal node-to-node links (internal_links), broken links (broken_links), and orphaned nodes (orphaned_nodes). Used to reconstruct cross-references on restore. The orphaned_nodes array lists nodes with no inbound links from other exported nodes.

In v4, broken_links is always [] — the export serializes only the live node_links index, which by construction contains resolvable node-to-node links, so no broken links can be recorded. The field is retained for schema stability.

VersionIntroducedNotes
v1InitialMarkdown-only revisions.
v2Added links.json.
v30.1Added .json files alongside .md for canonical BlockNote content; content under pages/.
v40.2 (current)Collapsed collections + pages into a single nodes tree (folders + pages); content under nodes/; added parent_id, position, deleted_at, include_trash, and node_properties.

Restore is backward-compatible: any older bundle restores cleanly into a current Marrow workspace. v1/v2/v3 bundles are auto-upgraded — their legacy collection/page layout is mapped onto the v4 node tree on read.

Terminal window
unzip -l marrow-export-mydocs-20260101T120000Z.zip
unzip -p marrow-export-mydocs-20260101T120000Z.zip manifest.json | jq .

If you want to verify a backup is restorable without disturbing your live instance, restore it into a fresh database:

Terminal window
# `-v` drops the Postgres volume so the DB is genuinely empty — otherwise
# restore collides on slugs that already exist in a non-empty database.
docker compose down -v && docker compose up -d
cd api && alembic upgrade head
marrow restore /path/to/bundle.zip