Skip to main content

CLI tool for backing up Grist documents as SQLite files

Project description

grist-backup

CLI tool to back up Grist documents as their native SQLite (.grist) files — equivalent to the Download → Document (with history) action in the Grist web UI.

Written in pure Python standard library: zero runtime dependencies.

Features

  • Downloads the original SQLite .grist file (full edit history by default)
  • Multiple accounts in a single config file (e.g. work + personal)
  • Explicit doc list or auto-discovery via the API (optionally scoped by workspace name)
  • Auto-discovers document name, workspace and team via the Grist API
  • Configurable subdirectory layout by account / team / workspace
  • xz (LZMA) compression by default — .grist files compress extremely well
  • Streaming download + on-the-fly compression (no double disk write)
  • Atomic writes via temp file + rename
  • Clear exit codes for scripting / cron jobs
  • All errors logged to stderr

Installation

Requires Python 3.11+.

pip install grist-backup

From source (development)

git clone https://github.com/lmzr/grist-backup.git
cd grist-backup
uv sync
uv run grist-backup --help

Configuration

Create a TOML config file in one of (searched in order):

  1. ./grist-backup.toml (current directory)
  2. ~/.config/grist-backup/config.toml
  3. ~/.grist-backup.toml

Or override with --config /path/to/file.toml.

Generate an example config file with:

grist-backup --example-config > ~/.config/grist-backup/config.toml

Example:

backup_dir = "~/grist_backups"
subdirs = ["team"]     # optional — default: ["team"]

[accounts.perso]
api_key = "your-api-key-here"
server  = "https://docs.getgrist.com"

# Only `id` is required. Name is auto-fetched from the Grist API.
[[accounts.perso.docs]]
id = "abc123DEF456"

# Override the fetched name explicitly:
[[accounts.perso.docs]]
id   = "xyz789UVW012"
name = "budget_override"

[accounts.work]
api_key = "another-api-key"
server  = "https://grist.mycompany.com"

[[accounts.work.docs]]
id = "mno345PQR678"

Lock the file down — it contains API keys:

chmod 600 grist-backup.toml

Self-hosted Grist instances

The tool works with self-hosted Grist (grist-core). All API endpoints used are part of the core project. A few things to keep in mind:

server must point to the root URL — not to an org-specific path:

# Correct:
server = "https://grist.example.com"

# Wrong (will break /api/orgs discovery):
server = "https://grist.example.com/o/myorg"

On self-hosted multi-org instances (GRIST_SINGLE_ORG unset), Grist uses URL path prefixes (/o/orgname/) to distinguish teams. The /api/orgs endpoint lives at the root level and won't respond correctly if the server URL already includes an org path.

On single-org instances (GRIST_SINGLE_ORG=myorg), the root URL is the only option — no special handling needed.

Metadata auto-discovery

For each document, grist-backup calls GET /api/docs/{docId} to retrieve its name, workspace and team — unless the name is already set in the config and subdirs doesn't reference team or workspace (no API call needed in that case).

If the metadata call fails (no network, bad key, deleted doc…), the tool falls back silently: doc name defaults to the doc id, and any missing team / workspace subdirectory level is dropped.

Subdirectory layout (subdirs)

Controls how backups are organized under backup_dir:

Value Result
[] Flat — all files directly in backup_dir
["team"] (default) One subdir per team
["account"] One subdir per account config name
["account", "team"] Nested: account → team
["team", "workspace"] Nested: team → workspace
["account", "team", "workspace"] Full nesting

team and workspace come from the Grist API; account is the name you defined in the config ([accounts.<name>]).

Auto-discovery of documents

Instead of listing each doc in the config, you can let the tool fetch the list from the Grist API. The selection is hierarchical: everything, or one or more teams (in full), or specific workspaces within a team.

# Everything
[accounts.everything]
api_key  = "..."
server   = "https://docs.getgrist.com"
discover = true

# Scoped — array of entries, each qualifies a team
[accounts.scoped]
api_key  = "..."
server   = "https://mycompany.getgrist.com"
discover = [
  { team = "MyCompany" },                                        # whole team
  { team = "PartnerOrg", workspaces = ["Finance", "Product"] },  # scoped
]

# Or equivalent array-of-tables (better for many entries):
[accounts.scoped2]
api_key = "..."
server  = "https://mycompany.getgrist.com"

[[accounts.scoped2.discover]]
team = "MyCompany"

[[accounts.scoped2.discover]]
team = "PartnerOrg"
workspaces = ["Finance", "Product"]

If the same team is listed twice, once as "whole team" and once with workspaces, the whole-team entry wins and the scoped ones are dropped (logged at info level).

CLI flag --discover forces discovery at runtime (scope = everything), taking precedence over any discover / docs setting:

grist-backup -a perso --discover
grist-backup --all --discover       # back up everything for every account

Precedence per account:

config discover config docs CLI --discover Used
absent / false non-empty explicit docs
absent / false empty absent error (exit 2)
true / list discovery (with warning if docs also present)
present discovery (scope = everything)

Discovery enumerates teams via GET /api/orgs, then workspaces+docs via GET /api/orgs/{orgId}/workspaces. Because those responses already contain team and workspace names, per-doc metadata fetches are skipped during the download phase.

If the discovery API call fails (network, auth…), the account is treated as failed: exit code 2 with -a, or skipped with a warning under --all.

Getting an API key

In Grist: click your profile avatar (top right) → Profile SettingsAPI section → Create.

API keys inherit the user's permissions.

Usage

grist-backup (-a ACCOUNT | --all) [-d DOC_ID] [-o OUTPUT] [-c CONFIG]
             [--no-history] [--no-compress] [--discover]

Exactly one of -a or --all is required.

Flag Description
-a, --account Account name in config
--all Back up every document of every account in config
-d, --doc Document ID or config doc name (requires -a, overrides config docs list)
-o, --output Output file or directory (overrides backup_dir)
-c, --config Config file path (overrides default search)
--no-history Exclude edit history from the backup
--no-compress Disable xz/lzma compression
--discover Fetch the doc list from the API (overrides config)
--example-config Print an example config file to stdout and exit
-V, --version Show version and exit

Default filename: {doc_name}_{YYYY-MM-DD}_{HHMMSS}.grist.xz (without compression: .grist).

The timestamp format can be customized in the config file with timestamp_format (strftime syntax):

timestamp_format = "%Y%m%d"   # → budget_20260420.grist.xz

Example tree with default subdirs = ["team"] and --all across two accounts on different teams:

~/grist_backups/
├── MyCompany/
│   ├── budget_2026-04-16_081948.grist.xz
│   └── roadmap_2026-04-16_081948.grist.xz
└── Personal/
    └── notes_2026-04-16_081948.grist.xz

With subdirs = ["account", "team"]:

~/grist_backups/
├── perso/Personal/notes_2026-04-16_081948.grist.xz
└── work/MyCompany/
    ├── budget_2026-04-16_081948.grist.xz
    └── roadmap_2026-04-16_081948.grist.xz

Examples

Back up all documents for one account:

grist-backup -a perso

Back up every document of every account (ideal for cron):

grist-backup --all

Back up one specific document by ID, saved to a custom directory:

grist-backup -a work -d mno345PQR678 -o /mnt/nas/grist_backups/

Back up one document by its config name (the name field in [[accounts.<acct>.docs]]):

grist-backup -a perso -d budget_override

Back up uncompressed with a custom filename:

grist-backup -a perso -d abc123 --no-compress -o today_budget.grist

Without history (smaller file, no undo):

grist-backup -a perso --no-history

Exit codes

Code Meaning
0 All downloads succeeded
1 Partial failure (some succeeded, some failed)
2 Total failure or config/argument error

Decompressing .grist.xz files

macOS (native Archive Utility, or CLI):

unxz backup.grist.xz        # replaces file in place
# or
xz -d -k backup.grist.xz    # keeps the .xz file

Installing xz if missing: brew install xz.

Linux: unxz / xz -d (usually pre-installed).

Windows: 7-Zip or PeaZip.

Python:

import lzma, shutil
with lzma.open("backup.grist.xz") as src, open("backup.grist","wb") as dst:
    shutil.copyfileobj(src, dst)

The resulting .grist is a standard SQLite database — inspectable with sqlite3 backup.grist ".tables" or any SQLite GUI.

Automation (cron)

Example daily backup of every account at 3 AM:

0 3 * * * cd $HOME && /usr/local/bin/grist-backup --all >> /var/log/grist_backup.log 2>&1

The tool always logs to stderr — the redirection above captures both channels.

Development

uv run ruff check src/
uv run ruff format src/

Building & Publishing

# Build the package (creates dist/*.tar.gz and dist/*.whl)
uvx --from build pyproject-build

# Check that the README renders correctly for PyPI
uvx twine check dist/*

# Upload to TestPyPI
uvx twine upload --repository testpypi dist/*

# Upload to PyPI (production)
uvx twine upload dist/*

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

grist_backup-1.0.0.tar.gz (29.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

grist_backup-1.0.0-py3-none-any.whl (29.8 kB view details)

Uploaded Python 3

File details

Details for the file grist_backup-1.0.0.tar.gz.

File metadata

  • Download URL: grist_backup-1.0.0.tar.gz
  • Upload date:
  • Size: 29.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for grist_backup-1.0.0.tar.gz
Algorithm Hash digest
SHA256 1d97946c8b640cc142aa47f344353a9412bd3a26d63eb1743a4f7e3a305456dd
MD5 8ecc8a68f3bc91866f703423bd1f0851
BLAKE2b-256 96d4a1174fcabd283ae193dc641068e2587cb7265f8d4c9db86fbd87999435eb

See more details on using hashes here.

File details

Details for the file grist_backup-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: grist_backup-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 29.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for grist_backup-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a5677c24af2c17e4d186016dd1950cca31832c0a2dd8ba7d5026c08ed84e7ca5
MD5 dffcd47d6e1f518140523194506af9a0
BLAKE2b-256 4a8f7761c84fc5751f2e351f2b9140d9fddc66aaccb8287f0fd773aca7e9f695

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page