Introduction
Sekai takes chunk-level deduplicated snapshots of Minecraft Java region
files (.mca) and rebuilds the world from any snapshot quickly. It is
built for frequent hot-recovery — rewinding a live server after griefing
or a bad update — rather than long-term archival.
Identical chunk payloads are stored once and shared across snapshots via content-addressed storage, so an unchanged backup costs roughly one metadata row instead of a full world copy. Rollback reproduces the exact bytes captured for the snapshot, atomically and verbatim.
Sekai ships two things:
- the
sekaiCLI documented in this guide, - a reusable Rust library (
sekai-appoversekai-core) for server-management software. The library API is documented with rustdoc (cargo doc); this book covers operating the CLI only.
Design background lives in ARCHITECTURE.md: layering, the two-layer hashing model, and the MVCC/GC data model. The machine-readable output contract lives in docs/json.md.
Installation
Prebuilt binaries are attached to each GitHub Release
(sekai-<target triple>.tar.gz). Alternatively, with a Rust toolchain
installed (the pinned version is in rust-toolchain.toml):
cargo install sekai-cli
The installed binary is named sekai. To build from a checkout instead
(for development):
cargo install --path ./crates/cli
Verify the setup:
sekai --help
Quickstart
# Record the current world state (pause the server first; see
# "Server coordination")
sekai --store ./sekai-store backup ./world
# Preview what a backup would record, without writing anything
sekai --store ./sekai-store status ./world
# List snapshots
sekai --store ./sekai-store list
# Rebuild the world from snapshot 1 (overwrites region files)
sekai --store ./sekai-store rollback ./world 1
# Compare chunk NBT between snapshots 1 and 2
sekai --store ./sekai-store diff 1 2 --in overworld:0,0
# Name snapshot 2 for later reference
sekai --store ./sekai-store tag stable 2
# Preview unreferenced blobs, then collect them
sekai --store ./sekai-store gc --dry-run
sekai --store ./sekai-store gc
# Delete old snapshots (keep newest 10), then reclaim their blobs
sekai --store ./sekai-store prune --keep-last 10
sekai --store ./sekai-store gc
# Rebuild snapshot 1 into a fresh directory (live world untouched)
sekai --store ./sekai-store export 1 ./restored
# Inspect region files without touching anything
sekai debug scan ./world
--store names the backup store directory (created when missing). Every
command accepts --json for machine-readable output and, except list
and tag, --timing for a per-phase breakdown. backup, status,
rollback, diff, export, gc, and prune take --progress for a
stderr progress bar (refused with --json). backup, status,
rollback, diff, and export accept a scope
(--in, --region, --kind); nothing selected means the whole world.
Server coordination
sekai never touches the server process. Pausing writes around a backup
belongs to the caller or the administrator — never to the tool:
save-off(stop the server from writing region files),save-all(flush pending writes to disk),- run
sekai backup, save-on(resume).
Backing up a world that is being written concurrently can capture torn sectors. Reads tolerate trailing partial sectors the way vanilla does, but genuinely corrupt runs fail loudly instead of being backed up silently. Rollback and export read only from the store, so they need no server coordination beyond stopping the server before you let players back in — rollback overwrites live region files.
World layouts
Three server families share the .mca format but not the directory
layout. Pass the same path on every run:
- Vanilla: one world folder (
region/,DIM-1/,DIM1/, and since 26.1dimensions/minecraft/<name>/). - Bukkit-family (Bukkit/Spigot/Paper/Purpur, pre-26.1 layout): the
server root, holding
<base>/,<base>_nether/DIM-1/, and<base>_the_end/DIM1/, wherebaseis thelevel-name(worldby default). Paper 26.1+ migrates to the vanilla layout. - Plugin worlds (Multiverse et al.): arbitrary folders, detected by their contents.
The full namespace rules live in
ARCHITECTURE.md (“World Layouts”). Two practical
consequences: custom-dimension folders are content-hashed, so renaming
one orphans its history; and rollback restores moved folders through
discovered siblings when possible, failing loudly
(UnknownRegionPath) rather than writing somewhere wrong.
Concepts
- Snapshot: one recorded world state, numbered in creation order
(
listshows them oldest first). Snapshots store only fresh rows; reads resolve effective state through fallback, so metadata grows with changes, not with universe size. - Blob (CAS key): the exact raw chunk payload, hashed with framing
included. Identical payloads share one stored copy across all
snapshots; rollback restores these bytes verbatim, rewinding volatile
tags such as
LastUpdateto capture time. - Diff hash: an opt-in volatile view over decompressed NBT with non-essential tags excluded. It speeds up change detection only and never alters stored blobs.
- Tombstone: an explicit “chunk absent” row. A chunk deleted between backups is recorded as a tombstone, not as silence — this is what lets rollback delete it again faithfully.
- Derived state:
region_statefingerprints speed up the next backup. Wiping them costs at most one full ingest, never wrong data.
The authoritative definitions are in ARCHITECTURE.md (“Data & Hashing Model” and “History, State, and GC Model”).
Scope selection
backup, rollback, diff, and export accept a scope: the whole
world by default, or per-dimension areas. Entries compose by union;
--kind (repeatable, empty means all) applies to every area.
sekai --store ./sekai-store backup ./world --in overworld --kind region --kind entities
sekai --store ./sekai-store rollback ./world 3 --in overworld:0,0..31,31
sekai --store ./sekai-store diff 1 2 --in overworld:0,0
sekai --store ./sekai-store export 3 ./restored --region overworld:0,0
--in DIMselects a whole dimension;--in DIM:x,zone chunk;--in DIM:x0,z0..x1,z1an inclusive chunk rectangle.--region DIM:RX,RZselects every chunk of one region file (rectangle shorthand).--kindselects region families (region,entities,poi).
The scope is a filter, not a partition: a scoped backup reads as the truth for its scope only (out-of-scope coordinates record nothing and resolve through fallback), and a scoped rollback or export never touches files outside the scope. See ADR-0003.
Backup
sekai --store ./sekai-store backup ./world
Records the current world state as a new snapshot and prints the snapshot id, chunk/blob counts, tombstones, skipped regions, and carried chunks.
--with-diffadditionally derives volatile diff views alongside blobs (slower ingest; the hot path stays decode-free without it).--jobs Nsets the ingest worker count (0means one per CPU).--progressshows a stderr progress bar (refused with--json).- Scope flags (
--in,--region,--kind) restrict what is recorded; see Scope selection. - For a dry-run preview of the above, see
statusunder Inspection. - When the store directory is created by the backup, the CLI says so on stderr (human output only).
Unchanged regions are skipped by fingerprint; unchanged chunks inside changed regions resolve through fallback and cost no new blobs. A backup with no changes still writes one snapshot row.
Rollback
sekai --store ./sekai-store rollback ./world 1
sekai --store ./sekai-store rollback ./world @stable --in overworld
Rebuilds the world from a snapshot, overwriting region files atomically
(temp file in the target directory + fsync + rename; in-place
mutation never happens). The snapshot argument accepts <id> or @tag
(see Tags). Files are stamped with the snapshot time, and
volatile tags are rewound to capture values.
The default policy is strict:
- region files unknown to the snapshot (created afterwards) are deleted;
- chunks unknown to the snapshot vanish on rebuild;
- tombstoned chunks are removed (fully tombstoned regions delete the file, never leaving a header-only shell);
- a blob missing from CAS aborts loudly (corruption — a partial world would be worse than none).
Each behavior is configurable without changing the default:
| Flag | Effect |
|---|---|
--keep-post-snapshot-files | keep snapshot-unknown files instead of deleting them |
--keep-post-snapshot-chunks | keep live bytes for snapshot-unknown chunks inside rebuilt regions |
--keep-tombstoned-chunks | keep live bytes for tombstoned chunks; fully tombstoned files are left alone |
--on-missing-blob skip-chunk | skip chunks whose blob is missing (default abort) |
--on-missing-file derived-only|error | ignore sibling folders, or fail, instead of guessing a location (default sibling-first) |
- Scope flags (
--in,--region,--kind) rebuild and delete only inside the scope; see Scope selection. Combine with--timing/--jsonas usual. - Before rebuilding, the CLI echoes the target snapshot, scope, and policy to stderr (human output only).
Export
sekai --store ./sekai-store export 1 ./restored
Rebuilds a snapshot into a fresh directory. Unlike rollback, the live
world is never touched — export shares the rollback restore set but
writes every file under the output directory at its layout-derived path.
The snapshot argument accepts <id> or @tag (see Tags).
- The output directory is created when missing and must otherwise be empty; anything else fails loudly so export can never clobber data.
--flavor legacy|new|bukkitselects the output layout (legacy:region/,DIM-1/,DIM1/;new:dimensions/minecraft/<name>/;bukkit: split folders).--basenames the overworld folder for the bukkit flavor (level-name, defaultworld).- Scope flags (
--in,--region,--kind) restrict what is exported; tombstoned regions produce no files. See Scope selection. --on-missing-blob skip-chunkskips chunks whose blob is missing (defaultabort, as in rollback).- Only vanilla namespaces are derivable; custom dimensions fail loudly
(
UnknownRegionPath) instead of landing somewhere wrong.
Tags
sekai --store ./sekai-store tag stable 1
sekai --store ./sekai-store tag stable @prev --force
sekai --store ./sekai-store tag -d stable
sekai --store ./sekai-store tag
Tags give snapshots human-readable aliases so rollback targets stay
readable (rollback ./world @stable instead of an ID). Names use
[A-Za-z0-9._-], run 1–64 bytes, and are never all digits, so @123
cannot be confused with snapshot 123.
- Snapshot arguments to
rollback,export, anddiffaccept<id>or@tag; reports always carry the resolved numeric ID. - Creating over an existing name fails unless
--forcemoves it. Deleting a missing tag fails loudly. - Bare
taglists all tags in name order;listshows each snapshot’s tags alongside. - Tags are metadata only and constrain nothing: pruning a tagged snapshot drops its tags with it.
Maintenance
sekai --store ./sekai-store gc --dry-run
sekai --store ./sekai-store gc
gc collects CAS blobs no snapshot references. Planning is read-only;
applying re-verifies candidates against fresh metadata before unlinking,
so the two-phase shape (gc plan then apply) holds even though the CLI
runs both in one invocation. gc --dry-run prints the plan only, and
combines with --timing (the plan phase is measured; apply_ms is 0)
but not with --progress, which has no apply phase to report.
GC removes only orphan blobs, never metadata; snapshot deletion is
prune (fold-into-next-retained, then gc to reclaim).
Pruning snapshots
sekai --store ./sekai-store prune --keep-last 10 --dry-run
sekai --store ./sekai-store prune --keep-last 10
sekai --store ./sekai-store prune --before @stable
prune deletes old snapshots. Retention keeps the newest --keep-last
N intersected with --before and newer (at least one selector is
required); a selection retaining nothing fails loudly instead of wiping
the store.
Deletion folds oldest-first: each retired snapshot moves its
still-effective rows onto the next retained snapshot and drops rows
already superseded there, so every retained snapshot restores exactly
as before. Tags on deleted snapshots disappear with them. Pruning never
unlinks blobs — run gc afterwards to reclaim the dereferenced ones.
--dry-run prints the planned deletions only.
For a full secondary backup of everything — snapshots, metadata, and blobs — copy or archive the entire store directory to an external location. Per-command file writes are crash-ordered (blobs before metadata commits, unlinks after), so a copied store is self-consistent.
Inspection
These commands never write to the world or the store.
sekai --store ./sekai-store list
sekai --store ./sekai-store status ./world
sekai --store ./sekai-store diff 1 2 --in overworld:0,0
sekai debug scan ./world
listprints snapshots oldest first with raw Unix-millis timestamps (RFC 3339 rendering is human-output only). Tags pointing at each snapshot are shown alongside; see Tags.--statappends per-snapshot change statistics (fresh/tombstone/new-blob/effective counts). An empty store prints a note to stderr and exits 0.statuspreviews what a backup would record: the basis snapshot (latest,nullwhen the store is empty), changed regions (new and deleted files split out), chunks that would be newly recorded, tombstones, and blobs absent from CAS.cleanmeans nothing in scope differs from the latest snapshot; counts match a subsequent backup unless the world changes in between. There is deliberately nobackup --dry-run:statusis that command.diffcompares chunk NBT between two snapshots, or between the live world (--world) and a snapshot. One--in DIM:x,zselection keeps the single-chunk output; several selections switch to grouped output, omitting chunks without differences. Scope flags (--in,--region,--kind) select the compared chunks; see Scope selection. A chunk absent or tombstoned on a side diffs as an empty compound there — missing coordinates are never errors (see ADR-0006); only corrupt payloads and missing blobs fail loudly.--show-valuesprints concrete old/new values in human output.debug scanlists region files with size, mtime, chunk count, and header hash, plus per-phase timings with--timing.
Automation
Every command accepts --json: stdout carries exactly one JSON document,
stderr stays silent on success, and colors never appear. Every command
except list and tag accepts --timing, which prints a per-phase table in human
mode and merges the same block into the JSON document in --json mode.
--progress draws a stderr bar and is refused with --json.
Exit codes: 0 success, 1 runtime failure (stdout holds the error
envelope), 2 usage error (clap prints to stderr).
Dry runs by command:
| Preview | Dry-run means |
|---|---|
status | the whole command (backup has no --dry-run by design) |
gc --dry-run, prune --dry-run | plan only, nothing deleted |
rollback, export | none (rollback is destructive by definition; export targets an empty directory, so it is trivially reversible) |
The full contract — flag matrix, envelope, per-command payloads, and the compatibility promise — is docs/json.md. That page is the reference; this book does not duplicate it.
Troubleshooting
unknown snapshot: N— the id does not exist. Checklist; ids are never reused.blob missing from CAS: <hex>— store corruption (or a store copied incompletely). Rollback aborts rather than writing a partial world;--on-missing-blob skip-chunk(rollback/export) continues without the affected chunks. Restore the store from the secondary copy, then investigate.cannot derive region path for dim ...(UnknownRegionPath) — a custom-dimension region whose folder is unknown, or--on-missing-file errorrefusing to guess. Run it against a world tree with the same folder layout the backup came from so sibling derivation can work, or accept that the region cannot be placed.unsupported schema version— the store was written by a newer (or older) binary. The project is pre-release: recreate the store, don’t migrate.- Region parse failures name the file (
failed to process region file ...). Torn tails from interrupted saves are tolerated when unreferenced; genuinely corrupt sector runs fail loudly by design. - Slow commands: rerun with
--timing(release build) and compare phases; for backup-path slowdowns prefer--timing --json, which carries the per-region breakdown.