Documentation Menu

Backup and restore

7 min read

quillet backup writes a project's environments, documents, versions and files to one archive, and quillet restore puts that archive back, history and all, into empty environments.

Why a project needs a copy outside the CMS

Every save in Quillet is a version, so a mistake in one document is undone from its history. That history lives inside the environment, though. It cannot help when a whole environment is deleted, when a database is lost, or when a project has to move to another instance. For those cases you need a copy of the project that lives outside the CMS: one you keep wherever you keep your other backups, and one that comes back exactly as it was.

The CMS does not keep that copy for you. The archive is a file on your side, so the CLI is what writes it, and the CMS's part is the import route that reads it back in. Between the two, a restore is not a re-creation: the same keys, version numbers, authors and timestamps come back, with the same file ids, and the same documents are live.

What an archive keeps and a restore puts back

A backup reads a project into an archive, and a restore writes that archive into empty environments. The sections below follow the archive through both halves of that round trip, and then cover the fallback for a CMS that cannot import.

What an archive holds

A backup walks every environment of the project, or only the ones --envs names, and reads each of them in every locale of the project, collecting:

  • every document, folder settings (_index) and components (_schemas/) included;
  • every version of each document, with its body, message, author and time, and a record of which version is published;
  • the project's own record, its environments and the folders of the media library;
  • the metadata and the bytes of every file, with the bytes checked against the sha256 the API answered for them.

API keys and members stay out of the archive. You manage both in the dashboard, so the archive's manifest.json lists them under skipped, and the summary at the end of the run repeats it:

Skipped: keys (API keys are managed on the dashboard and are not exported.), members (Members are managed on the dashboard and are not exported.).

With --dir, the archive is a directory. Without it, the archive is a .tgz of that directory, named quillet-<project>-<YYYYMMDD-HHmmss>.tgz. The format is frozen at version 1. Two backups of an unchanged project differ only in their createdAt, so when you keep backups in git, the diff between two of them shows real changes and nothing else.

A backup holds drafts and private files, so the CLI writes it for its owner alone: mode 0600 for a .tgz, 0700 for a directory. The CLI builds the archive beside its target and renames it over the target only once it is complete, so a run that fails or is interrupted leaves the previous backup as it was.

A backup needs content:read. A project-wide CLI key or a personal token can read every environment. A key bound to one environment is refused the others, so a plain backup with such a key stops with exit code 4; pass --envs <its environment> to back up only the environment the key may read.

What a restore does

restore first stages the archive in one request. It then uploads the bytes of the files the project does not already hold, four at a time, and commits one transaction per environment. Version numbers, content hashes, messages, authors, timestamps, file ids and publish pointers all come back as they were, so whatever was live is live again. That is why --publish is refused here.

Every target environment must be empty. A restore reproduces a state; it does not merge into one. Merging one environment into another is what promotion is for. Aimed at an environment that holds documents, a restore stops before it writes anything and exits with code 3:

An import writes into an empty environment; these still hold documents.

The --env flag decides where the archive goes. Without it, every archived environment goes into the project's environment of the same key, and the project must have them all; if one is missing, the CLI names it and stops before staging anything. With --env <target>, the CLI restores one environment into the target: the archive's environment of that key if the archive has one, and otherwise the archive's default environment. Only the flag narrows a restore this way. A QUILLET_ENV or an env in .quilletrc never narrows a whole-project restore.

What a restore may write depends on the credential behind it. A CLI key bound to one environment is refused a restore into any other, with exit code 4, before anything is staged. A project-wide key restores anywhere in the project if it holds both content:write and content:publish, and a personal token restores anywhere if you are an editor or an owner of the project.

Each environment commits on its own. If one environment's transaction fails, the others still commit, the CLI names the failures, and the exit code is 3. Documents that the components now in force would refuse are restored anyway, and the CLI lists them as warnings.

The lossy fallback

--as-drafts is for a CMS without the import route. Instead of replaying history, it writes the newest archived body of each document as a new draft. It reuses files that have the same sha256, uploads the rest under new ids, and rewrites the file URLs in Markdown bodies to match. History, timestamps and file ids are lost. The CLI says so before it starts. With --as-drafts, --publish publishes what the archive had published.

How to

Sign in with a personal token, as described in Install and sign in, and work from a directory whose .quilletrc names the project. A project-wide CLI key with content:read can take the backup too, and one with content:write and content:publish can restore. Only an owner can create the empty environment below.

Back the project up to a .tgz in the working directory:

npx quillet backup
main: 1 documents, 5 versions
staging: 0 documents, 0 versions
Backed up handbook to /home/ada/backups/quillet-handbook-20261024-225210.tgz: 2 environments, 1 document, 5 versions, 1 asset (99 bytes).

Or back it up to a directory, replacing the one that is already there. When an archive already exists at the target, the command asks before it replaces it; --yes answers for you:

npx quillet backup --dir --out ./snapshot --yes

Create an empty environment to restore into. In the dashboard that is Settings → Environments → New environment. From the terminal, with your token:

npx quillet envs create <env>

See what a restore would do before you run it. A dry run stages the archive, prints the plan and then throws the staged import away:

npx quillet restore ./snapshot --env <env> --dry-run
0 assets to upload, 1 already held; main → restored (1 document)

Run the restore. At a terminal it asks before it writes anything; --yes answers for you:

npx quillet restore ./snapshot --env <env> --yes
Restored into handbook/restored: 1 document, 5 versions, 0 assets written, 0 files uploaded, 1 published.

Check that the history came back along with the documents:

npx quillet content list --env <env>
npx quillet content versions <key> --env <env>

The versions keep their original numbers, times and authors. To restore a .tgz instead of a directory, name the file in place of ./snapshot.

Limits

  • The CLI writes the archive. The CMS keeps no backups of its own, and the dashboard offers no way to download one.
  • A restore refuses an environment that is not empty. Merging is the job of promotion, not of the import.
  • A restore does not bring back members, API keys or the project's settings. Recreate them in the dashboard and mint new keys, because the keys' secrets were never in the archive.
  • A restore cannot reuse a file id that another project on the same instance already holds.
  • An archive taken with --no-assets has the file index but not the bytes. Do not make it your only copy of the files.
  • A folder that holds nothing but its _index is archived only by a CMS from 2026-09-19 or later. On an older CMS, no key leads to such a folder.
  • An archive is not encrypted. It holds drafts and private files, so store it where only its owners can read it.
  • --publish without --as-drafts is refused with exit code 2, because a full restore already puts back what was live.

Last updated