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

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, 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 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.

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

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.

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

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:

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

Frontend Integration

The TypeScript frontend in 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.

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

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 AccessMutex<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 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.

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 →