# How Meetily's SQLite Database Stores Meeting Data, Transcripts, and Summaries Locally

> Discover how Meetily's SQLite database locally stores meeting data, transcripts, and summaries using Tauri, migrations, and WAL mode for reliable persistence.

- Repository: [Zackriya Solutions/meetily](https://github.com/Zackriya-Solutions/meetily)
- Tags: how-to-guide
- Published: 2026-08-04

---

**Meetily persists all meeting‑related information in a local SQLite database managed by the `DatabaseManager` in its Tauri backend, using migrations for schema version control and WAL mode for reliable writes.**

Meetily stores every aspect of your meetings—metadata, transcripts, speaker information, and AI-generated summaries—in a local SQLite database, ensuring complete data privacy and offline access. This architecture centers on the **`DatabaseManager`** struct in [`frontend/src-tauri/src/database/manager.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/database/manager.rs), which handles database lifecycle, connection pooling, migrations, and write-ahead logging. Below is a complete breakdown of how Meetily implements this local-first storage system.

## Database Location and Initialization

Meetily places the SQLite database file in the user's application-data directory:

- **macOS:** `~/Library/Application Support/Meetily/meeting_minutes.sqlite`
- **Windows/Linux:** Equivalent platform-specific directories

The `DatabaseManager::new_from_app_handle` method constructs this path, checks for legacy `.db` files to migrate, creates the database if missing, and immediately applies all pending migrations. This guarantees that the schema always matches the current application version.

```rust
// Database initialization sequence (from manager.rs)
let db = DatabaseManager::new_from_app_handle(&app_handle).await?;

```

## Connection Management and Transactions

The `DatabaseManager` creates a pooled `SqlitePool` via `SqlitePool::connect`, which all queries share. For atomic operations, the manager provides `with_transaction`, an async helper that automatically commits on success or rolls back on failure.

```rust
// Example transaction usage pattern
db.with_transaction(|txn| async move {
    // Multiple operations that succeed or fail together
    sqlx::query!("INSERT INTO ...").execute(txn).await?;
    sqlx::query!("UPDATE ...").execute(txn).await?;
    Ok(())
}).await?;

```

## Write-Ahead Logging (WAL) and Durability

Meetily uses SQLite's WAL mode for better concurrency and crash recovery. The `DatabaseManager` implements specific safeguards:

- **Startup check:** Detects and removes corrupted `.wal` or `.shm` files, then re-opens the database
- **Shutdown cleanup:** Calls `PRAGMA wal_checkpoint(TRUNCATE)` to flush pending writes and delete WAL files

These measures prevent data corruption and reclaim disk space predictably.

## Database Schema and Migrations

All schema changes are version-controlled in `frontend/src-tauri/migrations/` and applied via `sqlx::migrate!("./migrations")`. The migration history reveals how Meetily's data model evolved:

### Core Tables (Initial Schema)

[`20250916100000_initial_schema.sql`](https://github.com/Zackriya-Solutions/meetily/blob/main/20250916100000_initial_schema.sql) establishes the foundation:

- **`meetings`** — `id`, `title`, `start_ts`, `end_ts`
- **`transcripts`** — `id`, `meeting_id`, `speaker_id`, `text`, `ts`
- **`speakers`** — `id`, `name`

### Extended Tables (Subsequent Migrations)

| Migration File | Purpose |
|----------------|---------|
| [`20251101000000_add_summary_backup.sql`](https://github.com/Zackriya-Solutions/meetily/blob/main/20251101000000_add_summary_backup.sql) | Creates **`summaries`** table for generated meeting summaries |
| [`20251223000000_add_meeting_notes.sql`](https://github.com/Zackriya-Solutions/meetily/blob/main/20251223000000_add_meeting_notes.sql) | Adds **`meeting_notes`** table for free-form notes linked to meetings |
| [`20251229000000_add_gemini_api_key.sql`](https://github.com/Zackriya-Solutions/meetily/blob/main/20251229000000_add_gemini_api_key.sql) | Stores Gemini API key in **`settings`** table |
| [`20250920155811_add_openrouter_api_key.sql`](https://github.com/Zackriya-Solutions/meetily/blob/main/20250920155811_add_openrouter_api_key.sql) | Adds OpenRouter API key to `settings` |
| [`20251006000000_add_audio_sync_fields.sql`](https://github.com/Zackriya-Solutions/meetily/blob/main/20251006000000_add_audio_sync_fields.sql) | Extends `meetings` with `audio_offset` and sync metadata |
| [`20251010153942_add_ollama_endpoint.sql`](https://github.com/Zackriya-Solutions/meetily/blob/main/20251010153942_add_ollama_endpoint.sql) | Ollama endpoint configuration in `settings` |
| [`20251105120000_add_pro_license_custom_openai.sql`](https://github.com/Zackriya-Solutions/meetily/blob/main/20251105120000_add_pro_license_custom_openai.sql) | Licensing data for custom OpenAI endpoints |
| [`20251110000001_add_speaker_field.sql`](https://github.com/Zackriya-Solutions/meetily/blob/main/20251110000001_add_speaker_field.sql) | Enhanced speaker tracking in transcripts |

## Storing Meeting Data: Complete Examples

### Inserting Meeting Metadata

When a user starts recording, Meetily creates a meeting record:

```rust
sqlx::query!(
    "INSERT INTO meetings (title, start_ts) VALUES (?, ?)",
    "Q4 Planning Session",
    chrono::Utc::now().timestamp()
)
.execute(db.pool())
.await?;

```

### Saving Transcript Fragments

As Whisper produces transcription segments, they stream into the database:

```rust
sqlx::query!(
    "INSERT INTO transcripts (meeting_id, speaker_id, text, ts) VALUES (?, ?, ?, ?)",
    meeting_id,           // Foreign key to meetings.id
    speaker_id,           // Foreign key to speakers.id (or NULL)
    "Let's review the quarterly metrics.",
    chrono::Utc::now().timestamp()
)
.execute(db.pool())
.await?;

```

### Retrieving and Displaying Data

The frontend fetches transcripts through Tauri commands using ordered queries:

```rust
let rows = sqlx::query!(
    "SELECT text, ts, speaker_id FROM transcripts 
     WHERE meeting_id = ? ORDER BY ts ASC",
    meeting_id
)
.fetch_all(db.pool())
.await?;

```

### Storing AI-Generated Summaries

After processing, summaries persist to a dedicated table:

```rust
sqlx::query!(
    "INSERT INTO summaries (meeting_id, content, generated_at) VALUES (?, ?, ?)",
    meeting_id,
    "Key decisions: 1) Expand hiring... 2) Delay product launch...",
    chrono::Utc::now().timestamp()
)
.execute(db.pool())
.await?;

```

## Data Flow from Recording to Storage

1. **Audio capture** produces separate microphone and system audio streams
2. **Voice Activity Detection (VAD)** filters non-speech segments before sending to Whisper
3. **Transcription** returns text fragments that the backend inserts into `transcripts`
4. **Post-processing** generates summaries stored in `summaries` and notes in `meeting_notes`
5. **Querying** happens via Tauri commands that execute SQL against the pooled connection

## Key Implementation Files

| File Path | Responsibility |
|-----------|----------------|
| [`frontend/src-tauri/src/database/manager.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/database/manager.rs) | `DatabaseManager` struct: initialization, migrations, pool management, WAL cleanup, transaction helper |
| [`frontend/src-tauri/migrations/20250916100000_initial_schema.sql`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/migrations/20250916100000_initial_schema.sql) | Core tables: `meetings`, `transcripts`, `speakers` |
| [`frontend/src-tauri/migrations/20251101000000_add_summary_backup.sql`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/migrations/20251101000000_add_summary_backup.sql) | `summaries` table for meeting summaries |
| [`frontend/src-tauri/migrations/20251223000000_add_meeting_notes.sql`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/migrations/20251223000000_add_meeting_notes.sql) | `meeting_notes` table |
| `frontend/src-tauri/migrations/*.sql` | All schema evolution tracked as versioned migrations |

## Summary

- **Local-first architecture:** All data stays on-device in a single SQLite file
- **Versioned schema:** Migrations in `frontend/src-tauri/migrations/` ensure reliable upgrades
- **Reliable writes:** WAL mode with corruption detection and explicit checkpointing
- **Flexible storage:** Separate tables for meetings, transcripts, speakers, summaries, and notes
- **Atomic operations:** `with_transaction` helper prevents partial updates

## Frequently Asked Questions

### Where does Meetily store my meeting data?

Meetily stores everything in a single SQLite file at `~/Library/Application Support/Meetily/meeting_minutes.sqlite` on macOS (with equivalent paths on Windows and Linux). This file contains all meetings, transcripts, speakers, summaries, and settings—no cloud required.

### How does Meetily prevent data loss if the app crashes?

The `DatabaseManager` uses SQLite's WAL (Write-Ahead Logging) mode. On startup, it checks for and removes corrupted WAL/SHM files. On shutdown, it runs `PRAGMA wal_checkpoint(TRUNCATE)` to ensure all data is fully written to the main database file. Transactions also guarantee that multi-step operations complete entirely or not at all.

### Can I access my Meetily data from other applications?

Yes—the SQLite file is standard and queryable with any SQLite client. The schema follows migration files in `frontend/src-tauri/migrations/`, with core tables including `meetings`, `transcripts`, `speakers`, `summaries`, and `meeting_notes`. Note that schema changes may occur with app updates.

### What happens to my data when Meetily updates?

The `DatabaseManager` automatically runs `sqlx::migrate!("./migrations")` on startup, applying any new migration files in version order. This ensures your existing data is preserved while the schema evolves to support new features.