@restora/cli
v0.8.0
Published
Local, privacy-first scheduled Notion backups to your own storage — Google Drive, S3 (AWS/Backblaze B2/Cloudflare R2/Wasabi/DO Spaces), or a local folder. Runs on your machine — nothing is stored on any server.
Maintainers
Readme
Restora CLI — scheduled Notion backups to your own storage
Local, privacy-first automatic backups. The CLI runs on your machine, holds your credentials
locally (~/.restora/config.json, owner-only), and streams your Notion workspace straight into a
destination you own — Google Drive, any S3-compatible bucket (AWS, Backblaze B2, Cloudflare R2,
Wasabi, DigitalOcean Spaces), or a plain local folder. No Restora server is ever involved — there
is nothing on our servers to leak.
Why this design:
- Notion internal-integration token (not OAuth) → it never expires, so scheduled backups never hit the 30-day OAuth reconnect wall, and the token never leaves your machine.
- You own the destination → Google Drive uses the
drive.filescope (the tool only sees files it created); S3 uses your own bucket + keys; local is just a folder on disk. Nothing is stored by us.
For users — one command
npx @restora/cli setupThe wizard walks you through it: connect Notion → share your databases → connect Google Drive (one click)
→ pick a daily time → first backup. Re-run anytime to change things. Prefer it installed?
npm i -g @restora/cli, then restora setup.
Getting stale behavior?
npxcaches packages aggressively and can keep running an old build even after a new version is published — flags silently ignored, old output formats. Force the current release withnpx @restora/cli@latest …, or if installed globally,npm i -g @restora/cli@latest.
Other commands:
restora status # what's connected, schedule, last backup
restora backup # back up now → Google Drive
restora backup --databases id1,id2 # back up a subset (remembered for scheduled runs)
restora backup --all # back up everything, and widen scheduled runs to everything
restora scope # what scheduled runs currently cover
restora scope --restore # undo the last --all widening, back to your saved subset
restora backup --to local --dir ./bk # back up to a local folder instead
restora connect s3 # connect your own S3-compatible bucket (see below)
restora backup --to s3 # back up to your bucket
restora backup --keep 30 # keep only the newest 30 backups
restora backup --keep-weeks 8 # also delete backups older than 8 weeks (newest is always kept)
restora schedule --daily 02:00 | --remove
restora guard # snapshot BEFORE letting an AI agent loose (see below)
restora guard --report # exactly what that agent changedFaster repeat backups: --incremental
Re-download only what changed since your last backup. Rows Notion reports as untouched are reused from your previous file; everything else is fetched fresh.
restora backup --incremental # uses your most recent local backup as the base
restora backup --incremental --from prev.jsonRestora refuses to reuse anything it can't fully vouch for: if a database's schema changed, or the prior file came from a different backup format, that database is downloaded in full. False positives (a needless re-download) are fine; a missing row never is.
A backup that had to skip anything will not delete older backups, whatever your --keep setting —
retention should never trade a complete backup for a partial one.
If a row can't be re-read from Notion, the copy from your previous backup is kept rather than dropping the row — the run tells you which rows those are, and that they may be out of date.
Fixed in 0.5.1 — please re-run any incremental backup made with an earlier version. Before 0.5.1,
--incrementalcould attach the wrong file to a row it carried over from the previous backup (attachments were re-keyed per run, and the keys collided). Text, properties, blocks and relations were never affected, and the file itself still validates —restora verifypasses, because the reference resolves; it just resolves to a different attachment. A freshrestora backup(full, no--incremental) produces a correct file. Full backups, the web app, and scheduled cloud backups were never affected.
Inspect your backups (offline, read-only)
Point these at your backup files — the newest in your local folder by default, or any file with
--file <path>. All run entirely on your machine, make no network calls, and never change anything.
They return proper exit codes, so they drop straight into CI or a pre-commit hook.
restora doctor # one-shot health check: connections, latest backup, integrity, freshness
restora validate # is this a well-formed backup file? (exit 1 if not)
restora verify # integrity: files present, decode cleanly, relations intact (exit 1 if not)
restora stats # counts: databases, rows, views, relations, files
restora inspect # duplicate rows/databases, broken rollups, detached pages
restora tree # the workspace hierarchy at a glance
restora search "launch email" # full-text search across titles, properties, and page content
restora diff old-backup.json new-backup.json # what changed between two snapshots (databases, rows, schema)Same pure engines as the web app and the MCP server — just printed to your terminal.
Before you let an AI agent loose: restora guard
AI agents — Notion AI, MCP-connected assistants, bulk-edit scripts — can now edit and delete across your
whole workspace in seconds. guard is the seatbelt:
restora guard --label "before-cleanup-agent" # full snapshot, kept out of your normal backup history
# ... let the agent run ...
restora guard --report # rows added/removed/EDITED, schema changes, pages — offlineThe report names exactly what changed (row and page titles, property changes), and both snapshots are
full backups — if the agent broke something, restore the "before" file at
app.restora.cc. For pipelines: --json prints the diff as JSON, and
--fail-on-changes exits 1 when anything changed (CI gate for "this automation must not touch Notion").
Migrate in, export out (free, local)
restora import-obsidian ./MyVault --parent <notion-page-url> # Obsidian vault → Notion, images included
restora export-md <notion-page-url> --out page.md # any Notion page → clean Markdown
restora export-md <url> --images download # ...with images saved alongside
restora export-comments <notion-page-url> # every comment on a page → Markdown
restora export-comments <url> --format csv # ...or a spreadsheet-ready CSVAll of these run entirely on your machine with your own token — migration is never paywalled.
export-comments walks the whole block tree, so inline comment threads on individual blocks are
included, not just page-level ones — each thread is grouped and labeled with the text it's attached to.
Two Notion API limits, stated up front: only unresolved comments come back (resolved threads are
unreachable by every tool), and your integration needs the "Read comments" capability
(notion.so/profile/integrations → Capabilities — the command tells you if it's missing). Add
"Read user information" too if you want real author names instead of ids.
Bring your own bucket (S3-compatible)
restora connect s3 connects AWS S3, Backblaze B2, Cloudflare R2, Wasabi, or DigitalOcean Spaces — no
Restora setup required, you just supply your own bucket + keys (stored locally only, never sent to us). It
asks for the provider, region/endpoint, bucket, and an access key/secret (use a key scoped to just that
bucket: PutObject, ListBucket, DeleteObject). Then restora backup --to s3. Note: on S3, retention
(--keep / --keep-weeks) permanently deletes old backups — enable bucket versioning if you want
recoverability.
Use your backups from any AI agent (MCP)
restora mcp runs a local, read-only MCP server that lets any
MCP-capable AI agent — Claude Desktop, Cursor, Cline, Continue, Zed, ChatGPT desktop, and more — read
the backups already on your machine. Ask "what was in my Projects database last week?", "map my whole
workspace", or "what changed since this backup?" and the agent answers by calling the server's tools.
Privacy by design: Restora makes no LLM calls, holds no API key, and — in its default mode — makes zero network calls. Your own agent does the reasoning over local JSON. Every tool is read-only, so even if a backup contains malicious text it can't trigger any action. (The store-nothing posture, but stronger.)
Get the config snippet to paste into your agent:
restora mcp --print-configThen add it to your agent (same shape everywhere):
{ "mcpServers": { "restora": { "command": "npx", "args": ["-y", "@restora/cli", "mcp"], "env": {} } } }By default it reads your local-backup folder (~/Restora Backups, or whatever you set). Point it elsewhere
with --dir <folder> or --file <backup.json>.
Tools exposed: list_backups, describe_backup (workspace map: databases, properties, relations, views),
query_database (rows with readable values; relations resolved to the linked page titles), get_page
(a page as Markdown), search, and read_id_map (old→new ids after a restore, to repoint
integrations/webhooks).
Live drift audit (opt-in): add --allow-live to also expose run_drift_audit, which compares a backup
against your current Notion (deleted/emptied databases, removed properties, type changes) — the input
for a restore plan. This is the only tool that uses the network, and only to your own Notion (it needs
restora connect notion); it never calls an LLM or a Restora server.
Very large workspaces (memory)
Backups stream to disk as they're fetched — each database and file is written out and released, so
even multi-GB workspaces don't need a big heap (upload destinations briefly use ~the backup's size in
temp disk while shipping). The CLI also raises Node's heap automatically for backup as
belt-and-suspenders. If a truly enormous single database still runs out of memory:
restora databases # list your databases + their ids
restora backup --databases id1,id2 # back up a batch; repeat for the nextNote: a relation from a database in one chunk to a database in another chunk won't resolve across separate files — group related databases into the same run where you can.
Scope is sticky — and reversible
--databases/--pages are remembered, so unattended scheduled runs use them too. --all does the
opposite: it clears the saved subset, so scheduled runs go back to covering everything. That is
usually what you want, but it outlives the run you typed it on, so the CLI now says exactly what it
cleared and keeps it:
restora scope # what scheduled runs cover right now
restora scope --restore # put back the subset the last --all clearedBehind a company proxy or firewall?
Node.js ignores your browser's and your system's proxy settings, so Notion can open fine in your
browser while every command-line tool fails with a bare fetch failed. Restora reads the standard
variables — set one and re-run:
set HTTPS_PROXY=http://proxy.company.com:8080 # Windows (cmd)
export HTTPS_PROXY=http://proxy.company.com:8080 # macOS / LinuxHTTP_PROXY and NO_PROXY are honoured too, and credentials can be embedded
(http://user:pass@host:8080). If your company inspects TLS (Zscaler, Netskope and similar), also
point Node at its root certificate with NODE_EXTRA_CA_CERTS=/path/to/company-root.pem — never
disable certificate checking, since that would expose the token you're about to send.
When a connection fails, Restora probes DNS, TCP and TLS and prints which one broke rather than
repeating Node's fetch failed. Full guide: https://restora.cc/docs/cli-network.
Limitations (stated honestly)
- Scheduled backups run only when this machine is on and awake at the scheduled time.
- Scheduling writes a small runner to
~/.restoraand registers it with the OS scheduler (Windows Task Scheduler / cron). Logs go to~/.restora/backup.log. - Files up to 150MB per file are bundled by default. The CLI stores nothing and cannot know
your Restora plan, so it uses the Pro cap for everyone — tune it in either direction with the
FILE_MAX_BYTESenvironment variable (bytes, e.g.FILE_MAX_BYTES=20971520for 20MB). A skipped file is named in the run's warnings and stays safe in Notion — but it is not in the backup: the reference kept alongside it is Notion's expiring signed URL, dead within about an hour. Restoring any file over 20MB needs the destination Notion workspace on a paid Notion plan (Notion's multi-part upload limit; free Notion workspaces accept uploads only up to 5MB).
