Documentation Menu

Backups

8 min read

quillet backup writes a whole project, every version and every file, to an archive you keep, and quillet restore puts it back into empty environments exactly as it was.

Why History is not a backup

The CMS keeps every version of every document, so you undo a bad edit in History and never need a backup for it. What History cannot survive is the loss of the database, a deleted environment, or an upgrade that goes wrong. For those you need a copy outside the CMS, taken on a schedule and tested before the day you need it.

A backup is a file that engineers own. Content managers do nothing for it, and the dashboard has no backup screen. The command line writes the archive, and the CMS only receives it back through its import route. Upgrades, migrations and health asks for one before every upgrade.

What an archive carries back into the CMS

What a backup holds

quillet backup reads the project through the management API and writes:

  • every environment, or only the ones --envs names;
  • every document in every locale, folder settings and components included;
  • every version of each, with its body, message, author and time, and which version is published;
  • every media folder and every file, with its bytes, each checked against the hash the CMS gave for it.

The archive format is frozen at version 1, so a later CLI can restore an archive taken today. Two backups of a project that has not changed are identical apart from the time they were taken.

A backup holds drafts and private files, so the CLI writes it readable by its owner only. It builds the archive beside its target and renames it into place only once it is complete, so a failed or interrupted run leaves the previous backup where it was.

What it does not hold

The archive does not hold members or API keys, and its manifest says so under skipped. It does not hold webhooks either. A restore never changes the project's own settings, such as its name, its locales and whether search is on. Keys and members are decisions an owner makes on the dashboard, and a restore that recreated them would hand out access that nobody granted a second time.

What a restore does

quillet restore stages the archive's metadata in one request, uploads the bytes of every file the project does not already hold, four at a time, and only then commits, in one transaction per environment. Everything comes back as it was: keys, version numbers, content hashes, messages, authors, times, file ids and bytes, and which version was live. What was published is therefore published again at once, which is why the CLI refuses --publish.

Without --env, each archived environment goes back into the environment of the same key, and every one of them must already exist. With --env <target>, the CLI restores one environment into <target>: the archive's environment of that key if it has one, otherwise the archive's default.

Bodies are not checked against the components in force now. A restore puts back what was valid when it was written. Anything the current components would refuse comes back with a warning on stderr instead of stopping the restore.

What a restore refuses

  • A target environment that holds any document at all, deleted ones included, gets 409 not_empty. The CLI says An import writes into an empty environment; these still hold documents. and exits 3. A restore reproduces a state, and merging into live content is promotion's job.
  • An environment the project does not have. The command lists the missing ones and stops before it sends anything.
  • A locale the project does not have.
  • A version history with a gap in it, or a version whose hash does not match its body.
  • A reference to a document the archive does not carry, or to a file that neither the archive nor the project holds.
  • A file id that this project, or another project on the same instance, already uses for something else. The answer is 409 asset_conflict, with the reason for each id.

If one environment's transaction fails, the others still commit. The CLI names the failures and exits 3.

Who may do it

A backup reads every environment, so it needs a project-wide CLI key with content:read, or a personal token. A restore writes and publishes, so it needs the editor role or a key with both content:write and content:publish. A key bound to one environment may restore into that environment only. The CLI refuses any other before it sends anything:

This key is bound to staging; restoring into prod needs a project-wide CLI key or a personal token.

How to

CLI

To take a backup, name the CMS and put a project-wide CLI key in the environment, not on the command line, where ps and your shell history would show it. The commands below run in the same terminal and use both:

export QUILLET_API_URL=http://localhost:3000
export QUILLET_MANAGEMENT_KEY='<keyId>:<secret>'
npx quillet backup --project <project>

It writes quillet-<project>-<YYYYMMDD-HHmmss>.tgz, stamped in UTC, in the current directory. --out <path> names the file, --dir writes a directory instead of a .tgz, and --envs prod,staging limits the environments.

To schedule it, run the same command from cron, a CI schedule or your orchestrator's job runner. Keep the key in that scheduler's secrets, not in the command, and copy each archive somewhere other than the machine running the CMS. Here is a crontab line for a nightly backup at 03:00, where <backups> is a directory with the CLI installed and QUILLET_MANAGEMENT_KEY is set at the top of the crontab:

0 3 * * * cd <backups> && npx quillet backup --api-url https://<cms.example.com> --project <project> --quiet

The CLI never deletes old archives. Keep as many as you need and prune them yourself.

Then test a restore, because a backup you have never restored is a guess. Create an empty environment with a personal token, since creating one is an owner's work:

export QUILLET_MANAGEMENT_KEY='<tokenId>:<secret>'
npx quillet envs create <restore-test> --project <project>

Rehearse the restore first. --dry-run stages the archive, prints what would be uploaded and committed, and throws the staged import away. It asks nothing:

npx quillet restore <archive>.tgz --env <restore-test> --project <project> --dry-run

Then restore for real. At a terminal, restore asks before it writes, and --yes answers for you:

npx quillet restore <archive>.tgz --env <restore-test> --project <project> --yes

Compare the result with the original, or read a page from it, then delete the rehearsal:

npx quillet envs delete <restore-test> --project <project> --yes

To restore after a loss, first create the project in the dashboard if it is gone. Give it the archive's locales and every environment the archive holds, under the same keys and empty. Then run npx quillet restore <archive>.tgz --project <project> --yes without --env. Afterwards, mint new keys, invite the members again, and set the project's settings and webhooks, because none of them are in the archive.

API

The CLI drives the import route for you. Call the route directly only to see or abandon an import that a restore left behind. A staged import lives for an hour, and then the CMS sweeps it away. Run restore with --verbose and you find the id in the paths it logs, …/import/<importId>/…. Read what the import still needs:

curl -s -u '<keyId>:<secret>' \
  'http://localhost:3000/api/v1/projects/<project>/import/<importId>'

Abandon it. This removes its staged rows and every byte it uploaded that no file has come to use:

curl -s -u '<keyId>:<secret>' -X DELETE \
  'http://localhost:3000/api/v1/projects/<project>/import/<importId>'

An import that was already partly committed keeps what it committed, because the environment that succeeded is already serving those files.

Limits

  • A restore does not bring back members, API keys or project settings, and the archive does not carry webhooks. Recreate them by hand after a restore.
  • A restore refuses an environment that is not empty, and it never merges; to bring changes into live content, use promotion.
  • A file id that another project on the same instance holds cannot be reused. An archive therefore cannot be restored into a second project on the instance while the first still has its files: 409 asset_conflict. Restore into the original project, or on another instance.
  • There is no dashboard screen for backups or restores, and no scheduler inside the CMS.
  • The staged metadata of one restore may be at most IMPORT_MAX_BYTES, 256 MB by default, measured after the CLI's gzip is decoded. A proxy in front of the CMS needs a limit at least as large on that path, or it cuts the restore at the edge.
  • Each file's bytes may be at most ASSET_MAX_BYTES, the same as for an upload.
  • restore --as-drafts, for a CMS without the import route, is lossy. It writes the newest version of each document as a new draft, gives every file a new id, and restores no history.
  • backup --no-assets leaves the bytes out. Such an archive restores only where the project still holds those files.

Last updated