@liiift-studio/foundry-backup
v1.0.2
Published
Backup CLI for Liiift foundry Sanity projects: raw document export plus a content-addressed asset mirror
Readme
@liiift-studio/foundry-backup
Backup CLI for the Liiift foundry Sanity projects (Darden, TDF, MCKL).
Each foundry repo keeps its own workflow file and its own secrets, so a failure is isolated to one foundry. The backup logic lives here, so a fix ships to all three with one publish instead of three hand-edited YAML files.
Usage
foundry-backup --project-id pdao575x --dataset production --exclude-ext zip| Flag | Purpose |
| --- | --- |
| --project-id <id> | Sanity project ID (required) |
| --dataset <name> | Dataset to back up (default production) |
| --out <dir> | Where the tarball is written (default ./backups) |
| --assets <mode> | Asset mirror target: none (default) or blob |
| --exclude-ext <list> | Extensions to skip in the mirror, e.g. zip |
| --blob-prefix <path> | Blob pathname prefix (default <projectId>/<dataset>) |
| --concurrency <n> | Parallel uploads, 1-24 (default 8) |
| --no-drafts | Exclude drafts from the export |
| --dry-run | Report without writing |
Environment: SANITY_AUTH_TOKEN (required), BLOB_READ_WRITE_TOKEN (with --assets blob).
The Sanity token needs the sanity.project.datasets/read grant. A Deploy Studio
token does not carry it — use an Editor robot token.
What it backs up, and what it does not
The document export runs with raw: true: every document is included (asset metadata
records too) and asset references are preserved, but asset binaries are not
downloaded. On MCKL that is 6 MB instead of 4.48 GB.
Sanity documents raw exports as intended for re-import into the same project. Restored into a fresh project, asset references dangle. So:
- Document export alone protects against content loss - a bad migration, a mass delete, editor error. This is the common case.
- Document export + asset mirror is what you need to rebuild elsewhere.
Order zips (order_*.zip) are excluded from the mirror by convention. They are
regenerated from the order document plus current font files by the site's checkout
code, and they dominate asset storage - typically the majority of a foundry's total.
Note a regenerated zip is not byte-identical to what a customer received:
fonts and EULA text may have changed since the order.
Asset mirror
Sanity asset IDs embed the content hash (file-<sha1>-<ext>), so an asset's bytes
never change under a given ID. The mirror uses that as the blob key, which makes
"already backed up" a key check and means nothing is ever re-downloaded. The first
run moves the corpus; later runs move only new assets.
Blobs are never deleted, so an asset removed from Sanity by mistake stays recoverable here - something a snapshot cannot offer.
Recovery
Project IDs: darden ef4kzoyv, tdf j5q6yxwa, mckl pdao575x. Every command below
needs an Editor robot token as SANITY_AUTH_TOKEN - a Deploy Studio token fails,
it lacks sanity.project.datasets/read.
Download the tarball from the repo's Actions run (artifact <foundry>-backup-<n>).
First: is this actually a restore?
For one or a few bad documents, do not import a backup. The Studio's document
history covers 90 days (maxRetentionDays) and reverts a single document surgically.
A full import is a blunt instrument by comparison.
Preferred path - rehearse into a scratch dataset, then swap
This is the recommended route for any restore that matters. It costs nothing and it is the only way to see what you are getting before you commit to it.
# 1. Restore into a throwaway dataset, never straight over production
npx sanity@latest dataset create restore-check
npx sanity@latest dataset import <backup>.tar.gz restore-check \
--replace --allow-assets-in-different-dataset --project-id <projectId>
# 2. Verify before trusting it. Expect the live count MINUS 13 system docs.
# Point a Studio at restore-check, or query it directly.
# 3. Only then apply to production, or repoint the site at the verified dataset
npx sanity@latest dataset delete restore-checkScenario A - content mistake, assets intact
The common case: bad migration, mass delete, editor error. Assets still live in the dataset, so references resolve and this genuinely works.
npx sanity@latest dataset import <backup>.tar.gz production \
--replace --project-id <projectId>--replace is a merge, not a rollback. It replaces documents whose IDs match and
leaves everything else alone, so anything created after the backup survives the
import. You get deleted documents back; you do not return the dataset to its
backup-time state. If you need exactly that state, import into a fresh dataset instead
of over the existing one.
Scenario B - dataset or project lost
npx sanity@latest dataset create production
npx sanity@latest dataset import <backup>.tar.gz production \
--replace --allow-assets-in-different-dataset --allow-failing-assets--allow-assets-in-different-dataset is required because a --raw export's asset
records carry the original project and dataset. --allow-failing-assets is what makes
the import complete, and its name is the honest summary: you get every document,
reference and relationship, and broken images and fonts. Asset records import as
metadata pointing at URLs that no longer resolve.
Closing that gap is the whole job of the asset mirror. Without it, Scenario B recovers structure but not binaries.
Add --skip-cross-dataset-references if references to another dataset block the
import and you cannot restore the datasets in dependency order.
Do not use
--allow-system-documents imports Sanity's internal system.* documents. They are
excluded from both export and import by design; an export being exactly 13 documents
short of count(*[!(_type match "sanity.*")]) is those, not data loss.
Why there is no build step
This package ships its source directly: no tsup, no dist/. It deviates from the
other @liiift-studio packages on purpose. It is the thing that runs when everything
else is broken, so it has as few moving parts as possible - three small dependencies
and no compile stage that could fail at publish time.
Testing
npm test