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

> Learn how ai-memory backs up wiki and SQLite data via POST /admin/backup and restores it using the ai-memory restore CLI command. Ensure data integrity and portability.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: how-to-guide
- Published: 2026-08-19

---

**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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/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`.

```rust
// 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`](https://github.com/akitaonrails/ai-memory/blob/main/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.

```rust
// 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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/config.toml) are accepted.

```rust
// 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.

```rust
// 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:

```bash

# 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:

```bash

# 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:

```rust
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:

```rust
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`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs) uses `rusqlite` online backup so the database stays writable.
- **Deterministic tarball** — `build_backup_tarball_file` in [`crates/ai-memory-mcp/src/admin.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/admin.rs) assembles a gzipped archive of `wiki/`, `db/memory.sqlite`, and optional [`config.toml`](https://github.com/akitaonrails/ai-memory/blob/main/config.toml).
- **Safe restore** — The `ai-memory restore` command in [`crates/ai-memory-cli/src/commands/restore.rs`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/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.