How ai-memory Backs Up and Restores Wiki and SQLite Data

ai-memory creates a consistent, portable backup by streaming a gzipped tarball containing the entire wiki directory, a live SQLite snapshot, and an optional configuration file through the POST /admin/backup MCP endpoint, while the CLI ai-memory restore command validates and extracts the archive back to the data directory.

The akitaonrails/ai-memory project implements atomic backup and restore operations for wiki and SQLite data using Rust’s standard and async ecosystems. The MCP server generates a deterministic tar.gz archive via an admin HTTP endpoint, and the companion CLI reverses the process with strict path validation and automatic migration replay. Understanding these mechanics helps operators safely migrate, replicate, or recover an ai-memory instance without downtime.

How the Backup Archive Is Built

The backup flow starts in crates/ai-memory-mcp/src/admin.rs inside the POST /admin/backup handler. It delegates to build_backup_tarball_file, which orchestrates a live SQLite snapshot and deterministic tar assembly without stopping writes.

SQLite Online Snapshot

The Reader component in crates/ai-memory-store/src/reader.rs exposes snapshot_to, which invokes SQLite’s built-in online backup API through rusqlite. This allows the database to remain writable while a consistent copy is written to a temporary file named memory.sqlite.

// crates/ai-memory-store/src/reader.rs
pub async fn snapshot_to(&self, dest_path: PathBuf) -> StoreResult<()> {
    self.with_conn(move |conn| {
        conn.backup(rusqlite::DatabaseName::Main, &dest_path, None)
            .map_err(StoreError::from)
    })
    .await
}

Because the backup is performed online, the rest of the application can continue reading and writing the store during the operation.

Tarball Assembly in the MCP Server

Once the snapshot is ready, build_backup_tarball_file creates a deterministic tar.gz archive using tar::Builder wrapped in a flate2::GzEncoder. The archive includes three controlled paths:

  • wiki/ — the entire wiki directory is appended with tar.append_dir_all("wiki", &state.data_dir.join("wiki")).
  • db/memory.sqlite — the temporary snapshot is inserted under this exact path.
  • config.toml — the optional configuration file is added only if it exists.

The function returns a tokio::fs::File handle, letting the MCP handler stream the gzipped tarball directly to the client.

// crates/ai-memory-mcp/src/admin.rs (excerpt)
async fn build_backup_tarball_file(state: &AdminState) -> anyhow::Result<tokio::fs::File> {
    // … snapshot SQLite …
    let mut tar_file = tempfile::tempfile()?;
    let encoder = GzEncoder::new(&mut tar_file, Compression::default());
    let mut tar = tar::Builder::new(encoder);
    tar.append_dir_all("wiki", &state.data_dir.join("wiki"))?;
    tar.append_path_with_name(&snapshot_path, "db/memory.sqlite")?;
    if cfg.is_file() { tar.append_path_with_name(&cfg, "config.toml")?; }
    // … finish & return file …
}

How the Restore Command Works

The inverse operation lives in crates/ai-memory-cli/src/commands/restore.rs and is exposed as the ai-memory restore subcommand. It enforces safety checks before extracting any data.

Archive Validation and Extraction

Before writing to disk, the command verifies that no other ai-memory process is running. Unless --force is passed, it also refuses to overwrite a non-empty data directory. The tarball is then opened with std::fs::File, decompressed via flate2::GzDecoder, and iterated as a tar::Archive.

Each entry is inspected by validate_restore_entry, which blocks unsafe paths, symlinks, and hard links. Only entries matching wiki/, db/memory.sqlite, or config.toml are accepted.

// crates/ai-memory-cli/src/commands/restore.rs
fn validate_restore_entry(path: &Path, entry_type: tar::EntryType) -> Result<()> {
    if !path.components().all(|c| matches!(c, Component::Normal(_))) {
        bail!("backup contains unsafe path: {}", path.display());
    }
    if entry_type.is_symlink() || entry_type.is_hard_link() {
        bail!("backup contains unsupported link entry: {}", path.display());
    }
    // allow only wiki/, db/memory.sqlite, and config.toml

}

After validation, unpack_checked_archive extracts the entries into the configured data directory.

Store Reopening and Migration Replay

Once extraction completes, the restore command reopens the store with Store::open(&config.data_dir). This step automatically applies any pending SQLite migrations and verifies that the restored database is valid.

// crates/ai-memory-cli/src/commands/restore.rs (excerpt)
let file = std::fs::File::open(&args.from)?;
let decoder = GzDecoder::new(file);
let mut archive = tar::Archive::new(decoder);
unpack_checked_archive(&mut archive, &config.data_dir)?;
// … reopen store to run migrations …
let _store = Store::open(&config.data_dir)?;

Programmatic Backup and Restore Examples

You can trigger a backup from the CLI by asking the running server to produce a tarball:


# Ask the server to produce a backup and write it locally

ai-memory backup --to backup.tar.gz

Restore the archive with the CLI, forcing overwrite if the data directory already exists:


# Stop any running ai-memory instance first

ai-memory restore --from backup.tar.gz --force

For custom integrations, call the same internals from Rust. The following snippet produces a backup file using the shared admin state:

use ai_memory_mcp::admin::build_backup_tarball_file;
use std::sync::Arc;
use ai_memory_mcp::state::AdminState;

// `state` is the shared admin state used by the server
let file = build_backup_tarball_file(&state).await?;

To restore programmatically, load the CLI configuration and invoke the restore runner directly:

use ai_memory_cli::commands::restore::run;
use ai_memory_cli::config::Config;
use ai_memory_cli::cli::RestoreArgs;

let cfg = Config::load()?;                     // loads data_dir, etc.
let args = RestoreArgs { from: "backup.tar.gz".into(), force: true };
run(&cfg, args)?;

Summary

  • Online SQLite snapshot — The Reader::snapshot_to method in crates/ai-memory-store/src/reader.rs uses rusqlite online backup so the database stays writable.
  • Deterministic tarballbuild_backup_tarball_file in crates/ai-memory-mcp/src/admin.rs assembles a gzipped archive of wiki/, db/memory.sqlite, and optional config.toml.
  • Safe restore — The ai-memory restore command in crates/ai-memory-cli/src/commands/restore.rs validates every tar entry, blocks unsafe paths and links, and reapplies migrations after extraction.
  • End-to-end integrity — Atomic snapshotting plus strict validation ensures consistent backup and restore operations for wiki and SQLite data.

Frequently Asked Questions

Can I back up ai-memory while the server is running?

Yes. The backup process uses SQLite’s online backup API via snapshot_to in crates/ai-memory-store/src/reader.rs, which copies the main database while it remains writable. You do not need to stop the MCP server to create a consistent snapshot.

What files are included in an ai-memory backup?

The tarball contains the entire wiki/ directory, a SQLite snapshot stored as db/memory.sqlite, and an optional config.toml if it exists in the data directory. No other files are packaged.

How does the restore command protect against malicious archives?

Every entry is passed through validate_restore_entry in crates/ai-memory-cli/src/commands/restore.rs. It rejects paths with parent or absolute components, blocks symlink and hard-link entries, and only allows extraction into the expected wiki/, db/memory.sqlite, and config.toml paths.

Why does the restore command reopen the store after extraction?

After unpacking, the command calls Store::open(&config.data_dir) to apply any pending database migrations and verify the restored SQLite file. This ensures the recovered schema is up to date and the store is immediately usable.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →