# How World Monitor Stores API Keys Securely in macOS Keychain and Windows Credential Manager

> Learn how World Monitor securely stores API keys in macOS Keychain and Windows Credential Manager. Discover zero plaintext persistence and single authorization prompts for enhanced security.

- Repository: [Elie Habib/worldmonitor](https://github.com/koala73/worldmonitor)
- Tags: how-to-guide
- Published: 2026-03-09

---

**World Monitor's Tauri-based desktop client stores API keys in a consolidated JSON vault within the OS-native credential store (macOS Keychain or Windows Credential Manager), ensuring zero plaintext persistence and only one authorization prompt per app launch.**

World Monitor (koala73/worldmonitor) is a cross-platform desktop application built with Tauri that handles sensitive API credentials without ever writing them to unencrypted files. According to the source code in [`src-tauri/src/main.rs`](https://github.com/koala73/worldmonitor/blob/main/src-tauri/src/main.rs), the application leverages the Rust `keyring` crate to interface directly with platform-specific credential managers, storing all secrets as a single encrypted JSON blob under the service name `world-monitor`.

## The Consolidated Vault Architecture

Unlike apps that create dozens of separate keychain entries, World Monitor uses a **consolidated JSON vault** pattern. All supported API keys are serialized into one JSON object and stored under a single entry named `secrets-vault`, accessed via the `keyring` crate's `Entry::new(KEYRING_SERVICE, "secrets-vault")` constructor.

| Platform | Credential Store | Storage Format |
|----------|-----------------|----------------|
| macOS | **Keychain** (service `world-monitor`) | Single JSON vault entry |
| Windows | **Credential Manager** (service `world-monitor`) | Single JSON vault entry |

This design reduces macOS authorization prompts from potentially 20+ separate entries to exactly one per application launch, significantly improving user experience while maintaining security.

## Loading and Caching Secrets at Startup

When the application launches, `SecretsCache::load_from_keychain()` in [`src-tauri/src/main.rs`](https://github.com/koala73/worldmonitor/blob/main/src-tauri/src/main.rs) attempts to read the consolidated vault. If the vault exists, the function deserializes the JSON into a `HashMap<String, String>`, filters for `SUPPORTED_SECRET_KEYS`, and stores the result in a **thread-safe in-memory cache** protected by `Mutex<HashMap>`.

If no consolidated vault exists (first-run or legacy install), the loader performs a one-time migration: it iterates over every supported secret key, reads any legacy per-key entries from the OS store, builds the map, writes the new vault, and deletes the old entries. This migration runs silently in the background, requiring at most one extra authorization prompt.

```rust
impl SecretsCache {
    fn load_from_keychain() -> Self {
        // Try the consolidated vault first – one prompt.
        if let Ok(entry) = Entry::new(KEYRING_SERVICE, "secrets-vault") {
            if let Ok(json) = entry.get_password() {
                if let Ok(map) = serde_json::from_str::<HashMap<String, String>>(&json) {
                    let secrets = map.into_iter()
                        .filter(|(k, v)|
                            SUPPORTED_SECRET_KEYS.contains(&k.as_str()) && !v.trim().is_empty())
                        .map(|(k, v)| (k, v.trim().to_string()))
                        .collect();
                    return SecretsCache { secrets: Mutex::new(secrets) };
                }
            }
        }
        // …migration logic follows…
    }
}

```

*Source: [/src-tauri/src/main.rs#L83-L114](https://github.com/koala73/worldmonitor/blob/main/src-tauri/src/main.rs#L83-L114)*

Once loaded, future secret accesses never hit the OS store again, avoiding repeated keychain prompts during normal operation.

## Atomic Write Operations

All updates occur through **atomic writes** to prevent data corruption. The `set_secret` and `delete_secret` Tauri commands first update the in-memory cache, then call `save_vault()`, which serializes the entire map to JSON and writes it back to the single vault entry via `Entry::set_password`.

```rust
fn save_vault(cache: &HashMap<String, String>) -> Result<(), String> {
    let json = serde_json::to_string(cache)
        .map_err(|e| format!("Failed to serialize vault: {e}"))?;
    let entry = Entry::new(KEYRING_SERVICE, "secrets-vault")
        .map_err(|e| format!("Keyring init failed: {e}"))?;
    entry.set_password(&json)
        .map_err(|e| format!("Failed to write vault: {e}"))?;
    Ok(())
}

```

*Source: [/src-tauri/src/main.rs#L217-L226](https://github.com/koala73/worldmonitor/blob/main/src-tauri/src/main.rs#L217-L226)*

The `set_secret` command validates keys against `SUPPORTED_SECRET_KEYS`, trims values, and uses a clone-then-commit pattern to ensure persistence succeeds before updating the live cache:

```rust
#[tauri::command]
fn set_secret(
    webview: Webview,
    key: String,
    value: String,
    cache: tauri::State<'_, SecretsCache>,
) -> Result<(), String> {
    require_trusted_window(webview.label())?;
    if !SUPPORTED_SECRET_KEYS.contains(&key.as_str()) {
        return Err(format!("Unsupported secret key: {key}"));
    }
    let mut secrets = cache.secrets.lock().map_err(|_| "Lock poisoned".to_string())?;
    let trimmed = value.trim().to_string();

    // Build proposed state, persist first, then commit to cache
    let mut proposed = secrets.clone();
    if trimmed.is_empty() { proposed.remove(&key); } else { proposed.insert(key, trimmed); }
    save_vault(&proposed)?;
    *secrets = proposed;
    Ok(())
}

```

*Source: [/src-tauri/src/main.rs#L308-L334](https://github.com/koala73/worldmonitor/blob/main/src-tauri/src/main.rs#L308-L334)*

## Frontend Integration

The TypeScript frontend in [`src/services/runtime-config.ts`](https://github.com/koala73/worldmonitor/blob/main/src/services/runtime-config.ts) interacts with the Rust backend via Tauri's `invokeTauri` function. The `setSecretValue` wrapper sanitizes input, invokes the secure commands, updates local runtime configuration, synchronizes with the sidecar process, and notifies other application windows via `localStorage` events.

```typescript
export async function setSecretValue(key: RuntimeSecretKey, value: string): Promise<void> {
  if (!isDesktopRuntime()) return;                     // only desktop
  const sanitized = value.trim();

  if (sanitized) {
    await invokeTauri<void>('set_secret', { key, value: sanitized });
    runtimeConfig.secrets[key] = { value: sanitized, source: 'vault' };
  } else {
    await invokeTauri<void>('delete_secret', { key });
    delete runtimeConfig.secrets[key];
  }

  // push to sidecar (best-effort)
  try { await pushSecretToSidecar(key, sanitized || ''); }
  catch (e) { console.warn(`[runtime-config] Failed to sync ${key}`, e); }

  // notify other windows
  try { localStorage.setItem('wm-secrets-updated', String(Date.now())); } catch {}
  notifyConfigChanged();
}

```

*Source: [/src/services/runtime-config.ts#L50-L78](https://github.com/koala73/worldmonitor/blob/main/src/services/runtime-config.ts#L50-L78)*

## Security Guarantees

The architecture implemented in koala73/worldmonitor provides four critical security properties:

- **Zero Plaintext Persistence** – Credentials exist only within the OS keychain or Credential Manager; no temporary files or config files contain API keys.
- **Single Authorization Prompt** – The consolidated vault design ensures users see exactly one keychain prompt per application launch, regardless of how many secrets are stored.
- **Atomic Writes** – The entire JSON blob writes in one operation; failures abort without leaving partial or corrupted data in the credential store.
- **Thread-Safe Access** – `Mutex<HashMap>` protects concurrent IPC calls from multiple windows or threads.

## Summary

- World Monitor stores API keys in a **consolidated JSON vault** within macOS Keychain or Windows Credential Manager using the Rust `keyring` crate.
- The **in-memory cache** (`SecretsCache`) loads once at startup and serves all subsequent reads, eliminating repeated OS prompts.
- **Atomic writes** via `save_vault()` ensure data integrity when adding, updating, or deleting secrets.
- A **one-time migration** automatically consolidates legacy per-key entries into the single vault format without user intervention.
- The frontend invokes secure Tauri commands (`set_secret`, `delete_secret`) through `invokeTauri`, maintaining a strict boundary between UI and credential storage.

## Frequently Asked Questions

### How does World Monitor prevent storing API keys in plaintext?

The application never writes credentials to disk as plaintext files. Instead, [`src-tauri/src/main.rs`](https://github.com/koala73/worldmonitor/blob/main/src-tauri/src/main.rs) uses the `keyring` crate to encrypt and store all secrets within the OS-native credential store (macOS Keychain or Windows Credential Manager) as a single JSON vault entry.

### Why does the app only prompt once for keychain access on macOS?

By using a **consolidated vault** pattern where all secrets live in one keychain entry named `secrets-vault`, the app avoids creating separate entries for each API key. The `SecretsCache::load_from_keychain()` method reads this single entry at startup, caching results in memory so subsequent accesses never trigger additional prompts.

### What happens to existing API keys when the app updates to the consolidated vault?

During startup, if the consolidated vault does not exist, the loader automatically migrates legacy per-key entries. It reads every supported secret from individual keychain entries, builds the consolidated map, writes the new vault, and deletes the old entries—all in one atomic operation requiring at most one extra authorization prompt.

### Is the credential storage thread-safe for concurrent operations?

Yes. The Rust backend uses `Mutex<HashMap<String, String>>` to protect the in-memory secrets cache. Both `get_secret` and `set_secret` commands acquire the lock before reading or writing, ensuring safe concurrent access from multiple frontend windows or asynchronous operations.