Silo’s transfer system moves content between instances, environments, and storage drivers. Every export is streaming — the first bytes leave immediately and no copy of your data is staged on disk first. Archives are reproducible tar.gz files ordered by collection and entry id, byte-for-byte identical from identical data. API key hashes are excluded by default so a content export you share with others ships no credentials.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/org-quicko/silo/llms.txt
Use this file to discover all available pages before exploring further.
Export
Export writes every project and environment, including empty ones, along with schemas, entries, and media. Use--out for a compressed archive or --dir for an on-disk directory tree.
--dir output and the fs storage driver share the same layout, so an fs-backed instance is already a live export.
Narrowing with —include
--include narrows an export to a project, an environment, or a single collection. Pass it more than once for multiple scopes. Leave it out for the whole instance.
include parameter per rule:
Controlling media with —media
--media (or ?media= on the HTTP route) controls how media library files are handled:
| Value | Catalog | Files |
|---|---|---|
all | Every asset | Every file in the library |
referenced | Only assets the moved entries point at | Those files only |
none | Every asset | No files — catalog only |
all for a whole-instance transfer, and referenced once --include narrows the scope. Use none when both instances share the same S3 bucket — the catalog still moves so filenames, folders, and URLs are preserved, but no bytes are transferred.
Exporting API keys
Pass--with-keys to include API key hashes in the archive. Omit it (the default) for a content export you share with others.
Import
Import reads an archive or directory and writes its content into Silo. Every imported entry is validated against the schema it lands under — there is no option to skip validation, and a rejected entry is counted and named in the result without stopping the import.Merge vs replace
merge (default)
Matches on
(project, environment, collection, id). Missing entries are inserted. Conflicts are resolved by the newest updated_at, then by the higher rev, then by source instance_id. Pass --prefer local|remote to override the whole rule.replace
Brings each collection the archive carries to exactly the archive’s state. Every entry in the archive is written. Then every entry the archive does not carry is removed. Collections the archive does not mention are left alone.
Deletions do not merge. Silo keeps no tombstones, so only
replace mode reflects a deletion made on another instance.An import is not atomic. A failure partway leaves earlier writes in place. Treat a failed import as unknown state. A
replace that stops partway never leaves a collection empty — it writes first and removes after — so the worst case is extra entries. Run the import again to remove them.--dry-run before applying it.
A dry run always reports
rejected: 0. It does not write schemas, so it has nothing to compare entries against. Use it to preview adds, updates, and schema conflicts — not to audit validation.Import result shape
rejections list holds up to 100 entries. The rejected count is always complete. Read it after every import — a source instance can hold data an older schema accepted, and those rows stop at the door.
Copy between instances
POST /api/copy pulls an export from another running Silo and feeds it through the same importer. The destination uses the source credentials for that one request and never stores them.
include and media work the same way as on an export. The destination sends include to the source so the source builds only what you asked for rather than the full instance.
Progress streaming
An import or copy normally says nothing between the request and the answer. Behind a proxy that closes quiet connections, a working transfer can look like a failed one. SendAccept: application/x-ndjson to receive one JSON object per line as work progresses:
progress line whenever the importer reports (every 200 entries and at each phase boundary), a bare heartbeat after each second of silence, and a final result or error line. The HTTP status goes out before the work begins — the first byte is the status line, and the outcome is the last line.
The admin UI’s Data Transfer page uses this stream to show live progress for running imports and copies.
Copying between environments on the same instance
Moving data between two environments on the same instance requires no archive. Promotingdev to staging, or seeding a fresh environment from prod, is a single request:
transfer:* claim. It uses the same collection and entry claims as the ordinary CRUD routes:
| Direction | Claim required |
|---|---|
| Source read | collections:<project>/<env>/*:schema:read, collections:<project>/<env>/*:entries:read |
| Destination write | collections:<project>/<env>/*:create, collections:<project>/<env>/*:schema:update, collections:<project>/<env>/*:entries:create, collections:<project>/<env>/*:entries:update |
| Replace mode (delete) | collections:<project>/<env>/*:delete, collections:<project>/<env>/*:entries:delete |
400. Media is stored per instance, not per environment, so it is already shared and none is copied.
The admin exposes this at Settings > Environment > Data Transfer with a preview-then-apply flow.
HTTP API reference
| Action | Endpoint |
|---|---|
| Export | GET /api/export |
| Import | POST /api/import |
| Copy from another instance | POST /api/copy |
| Copy between environments | POST /api/projects/{p}/envs/{e}/copy |
Required claims
| Operation | Claims required |
|---|---|
transfer:export | Also needs collections:*/*/*:schema:read and collections:*/*/*:entries:read (or narrowed equivalents) |
transfer:import | Also needs entries:create, entries:update, entries:delete on the destination scope |
transfer:copy | Requires both transfer:export authority on the source and transfer:import authority on the destination |
Archive format and on-disk layout
The on-disk layout produced by--dir and the fs storage driver are one and the same format. Every archive and every fs-backed instance carries a format_version — currently "1". Silo refuses to open or import data stamped with a version it does not recognize.
Size limits
Archives arriving over HTTP are checked twice before anything is written:| Setting | Default | What it limits |
|---|---|---|
[transfer] max_archive_size_mb | 1024 MB | The compressed archive size |
[transfer] max_extracted_size_mb | 4096 MB | The uncompressed size (read from archive headers; every file counts as at least 4 KB) |
413 with code archive_too_large. The message names the setting to raise. Both limits apply to /api/import and /api/copy. A file you name on the command line is not checked.
Raise both to copy a large instance. Both are configurable in silo.toml or from Settings > Configuration > Transfers in the admin.
Importing system records
An archive can carry Silo’s own records under_system. A selection cannot name them, so the import checks them after unpacking, against the same claims their own routes require:
| Records | Claim needed |
|---|---|
_keys | keys:import |
_media, _media_folders, _media_folder_moves | media:create (even with media=none); media:delete too when replace would empty them |
_variables for a project | create and entries:update on <project>/*/* |
_audit, _plugins, _scope_renames, any other _ name | Never imported — the request is a 400 |
403 that names the records and the claim. The check reads rows, not empty collections, so exporting an empty library loads with content permissions alone. The silo import CLI on the host is trusted and loads all of it.
Cross-driver migration
Export and import speak only the storage interface, so switching from SQLite to thefs driver (or back) is: