Blockchain-state backups
The three API-only backup routes, the per-chain schedule rows they write, what the cron actually captures to disk, and why this is not a restore path for your platform data.
NFT blockchain-state backups have three routes, three permission keys and a cron job, and no screen anywhere in the admin panel. There is nothing to click, nothing that reports a failure to you, and nothing that tells you a schedule exists. This page is the interface.
A backup here is a compressed snapshot of NFT rows and settings, written to the backend's local disk. It is not a database dump, it is not off-site, and there is no route that restores it. Nothing about this feature replaces Backup and restore, which is the real disaster-recovery story for the platform.
The three routes
Three separate permission keys, none of which is implied by access.nft.admin.
An operator who can create a schedule cannot necessarily list or delete it.
The payload
POST takes:
| Field | Required | Accepted | Rejected with |
|---|---|---|---|
chain |
Yes | ETH, BSC, POLYGON — nothing else |
400, "Invalid chain. Must be one of: ETH, BSC, POLYGON" |
schedule |
Yes | HOURLY, DAILY, WEEKLY, MONTHLY |
400, "Invalid schedule…" |
backupType |
Yes | FULL or INCREMENTAL |
400, "Invalid backup type. Must be FULL or INCREMENTAL" |
includeDisputes |
No | boolean, default false |
— |
enabled |
No | boolean, default true |
— |
runImmediately |
No | boolean, default false |
— |
runImmediately fires a backup about a second after the response, in the
background. It reports nothing back — success and failure both land only in the
backend log, under NFT.
DELETE takes { "chain": "ETH" } and 404s if no schedule exists for it.
Where a schedule lives
There is no backup-schedule table. Each schedule is a row in the platform
settings table, keyed nft_backup_schedule_<chain>, whose value is a JSON
document:
{
"chain": "ETH",
"schedule": "DAILY",
"backupType": "FULL",
"includeDisputes": false,
"enabled": true,
"lastRun": "2026-08-05T02:14:09.412Z",
"nextRun": "2026-08-06T02:14:09.412Z",
"createdAt": "2026-07-30T09:00:00.000Z"
}Two things follow from the key shape:
- One schedule per chain, at most. Posting a second schedule for
ETHoverwrites the first — there is no error and no history. nextRunis the whole scheduler. The cron acts whennow >= nextRun, and writes the next stamp fromlastRunplus the period. Editing the row by hand changes when the next backup fires; settingenabledto false stops it without deleting it.
The cron is the only scheduling authority
processNFTBackups is registered in the nft category and polls every 15
minutes. That is finer than HOURLY, the tightest schedule the route offers,
so a due backup fires within a quarter of an hour of its nextRun rather than a
full period late.
It reads every nft_backup_schedule_% row, skips the disabled ones, and runs
those whose nextRun has passed — FULL calls a full capture, anything else
calls the incremental one.
The backup module used to also keep an in-process setInterval for each
schedule. Its initialiser was exported and called from nowhere, so a schedule
created through the admin route only ever ran while that one process stayed up
and never survived a restart. Wiring it up alongside the cron would have been
worse — every backup would have run twice.
The cron is now the single authority. If you are reading an older note that tells you to start the scheduler after creating a schedule, ignore it: creating the settings row is the scheduling.
Per-schedule failures are isolated on purpose — one bad chain must not cost the
others their backup — and are logged individually. A run in which every due
schedule failed throws, so the job's status goes to failed with the error
rather than quietly reporting success.
What a backup actually contains
A FULL backup collects, for the whole marketplace and not just the named
chain:
- every
nft_collectionrow (the "contracts" list); - every
nft_tokenrow; - every
nft_listingrow; - every
nft_disputerow, only whenincludeDisputesis true; - every settings key beginning
nft, plus total sale volume, the active listing count and the token count; - the last seven days of
gas_historyfor that chain.
The block number of the chain at capture time goes in the metadata, which is where the "blockchain state" name comes from — the rows are the platform's record of the chain, not a re-read of it.
Two entries are always empty, and knowing that saves an investigation:
transactions is deliberately not collected (NFT movements live in the core
transaction ledger), and gasHistory is empty on every install because
nothing writes that table.
An INCREMENTAL backup takes only rows changed since the previous backup's
timestamp; with no previous backup on disk it falls back to a full one.
What it is not
- Not your users, wallets, balances, sales or offers.
nft_sale,nft_offer,nft_bid,nft_activityand every core table are outside it. - Not a restore path. The service has a restore method, and no route, screen or job calls it. Recovering from one of these files means a developer and a plan, not a button.
- Not off-site. S3 upload is stubbed out — the client is set to
nulleven when AWS credentials are present, so every backup is local disk only.
Where the files land
| Setting | Default | Effect |
|---|---|---|
BACKUP_PATH |
<backend working directory>/backups/nft |
Where the .gz files are written. Created if missing |
BACKUP_ENCRYPTION_KEY |
unset | When set, the JSON is AES-256-GCM encrypted before compression. A hex key — the file is unreadable without it, and there is no recovery if you lose it |
Filenames are nft-backup-<chain>-<full|incremental>-<block>-<timestamp>.gz.
Only a full backup carries a block number there: <block> is block-<n> for a
full capture, and the literal latest for an incremental one, which never
passes a block number. So an incremental file on disk reads
nft-backup-ETH-incremental-latest-<timestamp>.gz — there is no
-incremental-block-… to search for.
Retention is 30 files per chain, pruned oldest-first by modification time
after each successful backup, with one exception: the newest full backup is
never deleted, because an incremental is a delta with nothing to apply it to
otherwise.
Disk is your responsibility. A daily full backup of a large marketplace on three chains is three growing files a day against a 30-file ceiling per chain, and nothing on the platform warns you about the volume.
How to tell it ran
There is no backup record in the database. The service writes one only if a
systemBackup model exists, and this build has none, so nothing is written to
any table — including nft_metadata_backup, which exists in the schema and is
written by nothing in this build.
That leaves three ways to confirm a run:
-
The cron console —
/admin/system/cron, theprocessNFTBackupsrow. Status, last run, last error and the live log frames all key off the exact job name, so this is the authoritative surface. -
The files on disk — list
BACKUP_PATH(orbackups/nft) and check the newest timestamp for the chain you care about. -
The schedule row —
GET /api/admin/nft/backup/schedulereturns each config with itslastRunandnextRun. AlastRunofnullon a schedule older than its period means it has never fired.
A run that logs success has written a compressed file it has never read back. Restore-testing is not automated anywhere in this product. If these backups matter to you, decompress one by hand on a schedule of your own and confirm it parses — an encrypted backup whose key has drifted looks exactly like a healthy one until the day you need it.
Related
- Backup and restore — the platform-wide backup that actually protects your data.
- Scheduled jobs — the cron process, and what happens when it is not running.
- Permissions —
manage.nft.backup,view.nft.backup,delete.nft.backup. - Gas and the ecosystem master wallet — the
gas_historytable these backups try to collect.