Skip to content

Backup and restore

A Trokky backup is a zip file produced by the CLI over the HTTP API. It describes content, not storage, which is why it moves between backends: a backup taken from a Postgres instance restores into a filesystem instance and the other way round.

Both commands talk to a running server. There is no offline mode — you need a URL and a token, or a configured instance. See The CLI for how credentials are resolved.

Terminal window
trokky backup --output backup.zip

--output is required. The other flags:

FlagEffect
--collections posts,pagesBack up only these collections. Names must exist on the instance.
--skip-mediaSkip the media download entirely. Much faster, and the archive holds no files.
--description "before the migration"Free text stored in the manifest.
manifest.json
collections/post/8f2c….json
collections/post/a190….json
collections/homepage/home.json
media/hero.jpg
media/portrait.png

manifest.json records the format version (2.0), a timestamp, the source URL and description, every schema that was backed up, a dependency graph derived from reference and media fields, a restore order computed from that graph, an index of media by id, and document counts per collection.

Each document is one JSON file named after its id, exactly as the API returned it.

What is not in the archive: users, API tokens, webhooks, instance settings and audit logs. The backup fetches /collections and /media and nothing else. Restoring into a fresh instance gives you content, not accounts — you will need to create the admin user and any API tokens again. See Users and authentication.

Two limits worth knowing before you rely on this:

  • Each collection is read with limit=10000. A collection holding more documents than that is truncated, and the run reports the truncated count as a success.
  • Media files are stored in the archive under their original filename. Two media assets with the same filename collide, and on restore only one of them survives.
Terminal window
trokky restore --input backup.zip

--input is required. Restore refuses an archive whose manifest version is not 2.0.

FlagEffect
--collections posts,pagesRestore only these. They must be present in the backup.
--with-dependenciesAdd each selected collection’s dependencies, from the manifest’s graph.
--cleanDelete existing documents in the selected collections, and all media, before restoring.
--overwriteOn a failed create, retry as a PUT at the original id.
--dry-runReport the plan and validate the target. Writes nothing.

Before writing anything, restore fetches the target instance’s /collections and fails if any collection you asked for is missing there. Schemas are not restored — the target must already define them, which in practice means deploying the code first and restoring content second.

Then it works in a fixed order:

  1. Media first. Every file is uploaded, and each upload’s new id is recorded against the old one. Uploads are retried up to three times.
  2. Documents in dependency order, so a document is restored after the things it references.

Restore does not remove anything unless you pass --clean. Run it twice against the same instance and you get two copies of every non-singleton document, because each one is created with a POST.

This is the part that catches people.

  • Singletons keep their document id. Restore issues a PUT to the original id, which is an upsert for a singleton collection.
  • Every other document is created fresh and gets a new id. References to it are rewritten to match, in the same run.

Whether a collection counts as a singleton is decided by the singleton: true flag on the schema — checked on the target instance first, then in the backup’s manifest. A collection that presents as a singleton in structure.ts but does not declare it in its schema is treated as an ordinary collection here, and its id is regenerated. That is the single most expensive mistake in Trokky; it is written up in full under Things that will bite you and Singletons.

Reference rewriting runs three passes over each document: the schema’s reference and media fields, a deep scan for any asset._ref the schema pass missed, and a string replacement that catches media ids embedded in rich text HTML.

The consequence is the one stated in the traps page: anything outside the CMS that names a document by id — a hardcoded link, a redirect map, an external system — points at nothing after a restore. Address documents by slug if they need to survive one.

Before sending a document, restore removes the system fields id, _id, _createdAt, _updatedAt, _version, _revision, _collection and _type. Creation and update timestamps therefore reflect the restore, not the original edit. _status is not stripped, so drafts stay drafts and published documents stay published.

It then sanitises: null values are removed, objects that end up empty are removed, and media objects in the pre-2.0 src format are dropped rather than sent.

The backup is taken over the API, so nothing in it knows whether the source stored JSON files or Postgres rows.

Terminal window
# From the filesystem instance you have been developing against
trokky backup --url http://localhost:3000/api --token "$DEV_TOKEN" \
--output snapshot.zip
# Into the Postgres instance you just deployed
trokky restore --url https://cms.example.com/api --token "$PROD_TOKEN" \
--input snapshot.zip --dry-run
trokky restore --url https://cms.example.com/api --token "$PROD_TOKEN" \
--input snapshot.zip

Run the --dry-run first. It performs the same schema validation against the target as a real restore and tells you which collections are missing, which is the failure you want to find before you have half a site in place.

Adapter choice is otherwise a configuration question — see Storage adapters.

Terminal window
trokky restore --input backup.zip --collections post --with-dependencies

Without --with-dependencies, a selective restore brings in post alone. Any reference from a post to an author that was not restored still holds the backup’s old id, and nothing on the target resolves it. The reference is not cleared and no error is raised — you get a document pointing at an id that does not exist.

If you are restoring a subset, either pass --with-dependencies or list the referenced collections yourself.