Exporting an app
akter export copies every actor of an Akter Cloud app into one .akter archive on your machine. Each actor arrives as its own SQLite database, holding its state, its tables and its command history. Import the archive into a folder with akter import, then open the databases with sqlite3, serve them with akter dev, and read each actor's history with akter history.
This guide covers running an export, what it holds, how to resume one and how the CLI checks what it downloads. The flags are listed in the CLI reference.
Run an export
You need an API key with export scope for the organization. A key with only deploy, call or read scope is refused. Put it in AKTER_API_KEY and name the organization, the app and the archive to write:
export AKTER_API_KEY="<export-scoped key>"
bunx akter export \
--organization "$AKTER_ORGANIZATION" \
--app chat \
--url "$AKTER_CLOUD_URL" \
--out ./chat.akterThe key only reaches that organization's app. Every listing, snapshot and download request is authorized for it, and none of them reach another app or organization.
The CLI goes through the app's actors one at a time. For each one it prints progress every MiB and when the download finishes, then how many actors it exported, and last the archive it wrote:
Room/lobby: 1048576 of 2359296 bytes
Room/lobby: 2097152 of 2359296 bytes
Room/lobby: 2359296 of 2359296 bytes
Room/archive: erased, nothing to export
Exported 1 actors to /home/me/chat.akter.staging
Wrote /home/me/chat.akter with 1 entriesAn export is complete only when it prints Wrote <archive> with <n> entries and exits 0. Treat any other exit as a failed export and keep the message it printed.
What an export holds
The archive is a tar file compressed with zstd. Its first entry is manifest.json, which records the format version, when the archive was written, and the size and SHA-256 of every other entry. Its computers section lists every computer the app's actors keep: the actor, the computer's name, image, size and egress hosts, and its newest checkpoint when it has one. A computer's disk stays in Akter Cloud, so its checkpoint is recorded with "portable": false and the archive holds none of its files. Its lineage section has one entry per forked actor: the actor, the actor it was forked from (parent), the fork's id (fork) and the parent's commit version at the cut (forkedAt). The entries after it use the layout akter dev uses:
manifest.json
shards/<shard>/<Actor>/<key>/actor.db
shards/<shard>/<Actor>/<key>/files/<sha256>tar --zstd -tf chat.akter lists them, manifest.json first.
Each actor.db is a plain, decrypted SQLite database. It holds the actor's whole database at one consistent point after its last committed command:
- its state and its own tables,
- its command journal, which is the history
akter historyreads, - its pending timers, outbox and jobs.
Each files/<sha256> is one distinct file the actor's database names, its plaintext content named by its SHA-256; the file's paths, size and time stay in actor.db. The export holds nothing else: no Cloud keys, no export records and no file keys, since the CLI blanks every file grant in actor.db before it archives it.
Each actor is snapshotted on its own, after a commit and while no command runs on it, so each database is consistent. Actors are exported one after another, though, so two actors' snapshots come from different moments. Don't read an export as one snapshot of the whole app.
Use the exported data
Read the archive into a new folder. The CLI checks every entry against its manifest, takes each actor's files into the folder's own file home, files-home, and creates the folder only once every entry has passed and every file is in:
bunx akter import ./chat.akter --dir ./chat-exportThen serve the exported actors locally with the same app entry you deploy:
bunx akter dev --dir ./chat-exportOr do both in one step: bunx akter dev --from ./chat.akter --dir ./chat-export.
Read one actor's history from the folder:
bunx akter history Room/lobby --dir ./chat-exportVerify it, from your app's folder: --verify replays the actor's commands against the handlers you deploy now and reports no difference, or the first command whose outcome differs. Akter Cloud has no verify command of its own; this export, import and replay is how you verify a Cloud app's history.
bunx akter history Room/lobby --entry src/app.layer.ts --dir ./chat-export --verifyOr open a database directly with stock sqlite3:
sqlite3 ./chat-export/shards/<shard>/Room/lobby/actor.db ".tables"Erased actors
An erased actor has nothing to export. The CLI prints <Actor>/<key>: erased, nothing to export, writes nothing for it and goes on. Erased actors aren't counted in the final Exported <n> actors line.
Resume an interrupted export
The CLI downloads into a staging folder beside the archive, chat.akter.staging, and writes the archive only when every actor is in. While an actor downloads, actor.db.export holds the bytes so far and actor.db.export.json records the snapshot they come from. The file is renamed to actor.db only after it passes its check.
If an export stops partway, run the same command again with the same --out:
- An actor that already has an
actor.dbis kept as it is, and the CLI prints<Actor>/<key>: already exported. - A partly downloaded actor prints
<Actor>/<key>: resumingand continues from the same snapshot, starting where the kept bytes end. - Every other actor is snapshotted and downloaded as usual.
Kept actors aren't refreshed. To export the app as it is now, delete the staging folder or use a new --out. Once the archive is written, the staging folder is removed, and an --out that already exists is refused.
Starting an actor's export replaces that actor's previous snapshot on the server. Run one export of an app at a time, or one run can replace the snapshot another is still downloading.
How the download is checked
The snapshot stays encrypted on the server under a key of its own, and only the download decrypts it. When the server takes the snapshot, it also records the database's SHA-256 and seals the snapshot with a MAC over every page in order, which it checks before it reports the snapshot. The MAC covers the snapshot file only: a page rolled back inside the live actor.db on the server is outside what export can detect.
The CLI downloads at most 64 KiB per request and hashes each piece as it writes it. On a resume, it first hashes the bytes it kept, then continues. When the download is complete, it compares the hash with the one the server recorded and renames the file to actor.db only if they match. So a finished actor.db is byte for byte the database the server snapshotted.
When a check fails, the CLI deletes that actor's partial download and exits nonzero:
- The download doesn't match its SHA-256:
Room/lobby did not match its SHA-256. Run akter export again to start that actor over. - The snapshot was replaced, or failed authentication or its page MAC check on the server:
The snapshot of Room/lobby is gone (<reason>). Run akter export again to start that actor over.
Run the command again. Finished actors are kept, and the failed actor starts over from a new snapshot.
Known limits
- An imported fork still names the actor it was forked from by Akter Cloud's identity,
<Actor>/<key>, in its file records, and the folder's file home doesn't record that actor as its parent. Its state and files are served as usual, and the archive's manifest lineage still names its parent. Only the local record of where it came from stays in Cloud's form.