Storing API Keys Safely in a Tauri App — Don't Just Use LocalStorage

rust dev.to

All tests run on an 8-year-old MacBook Air. All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.

Users enter their Gemini API key into your app. Where does it go? The wrong answer is localStorage. The right answer is the system keychain.


Why localStorage Is Wrong

localStorage in a Tauri WebView is just a file on disk. On macOS:

~/Library/WebKit/com.yourapp.app/WebsiteData/LocalStorage/
Enter fullscreen mode Exit fullscreen mode

It's not encrypted. It's not protected by the keychain. Any process running as the same user can read it.

For an API key that costs the user money if leaked, this is not acceptable.


The Right Approach: System Keychain

For straightforward API key storage, use the OS keychain directly via the keyring crate:

# Cargo.toml
[dependencies]
keyring = "2"
Enter fullscreen mode Exit fullscreen mode
use keyring::Entry;

pub fn save_api_key(service: &str, key: &str) -> Result<(), AppError> {
    let entry = Entry::new(service, "api_key")?;
    entry.set_password(key)?;
    Ok(())
}

pub fn get_api_key(service: &str) -> Result<String, AppError> {
    let entry = Entry::new(service, "api_key")?;
    Ok(entry.get_password()?)
}

pub fn delete_api_key(service: &str) -> Result<(), AppError> {
    let entry = Entry::new(service, "api_key")?;
    entry.delete_password()?;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

On macOS, keyring stores in Keychain Access. The key is protected by the OS. Other processes can't read it without explicit user permission.

NOTE: tauri-plugin-stronghold はより高度な暗号化ストレージが必要な場合の選択肢です。API キー1つを安全に保存するだけなら keyring で十分です。


The Tauri Command Layer

#[tauri::command]
async fn save_api_key_cmd(key: String) -> Result<(), AppError> {
    save_api_key("com.yourapp.gemini", &key)
}

#[tauri::command]
async fn get_api_key_cmd() -> Result<String, AppError> {
    get_api_key("com.yourapp.gemini")
}
Enter fullscreen mode Exit fullscreen mode

The frontend never stores the key. It calls invoke('get_api_key_cmd') when it needs it. The key lives in the keychain, not in JavaScript memory or localStorage.


First-Launch UX

On first launch, get_api_key returns an error (key not found). Use this to show an onboarding screen — not an error dialog.

const apiKey = await invoke<string>('get_api_key_cmd').catch(() => null)

if (!apiKey) {
    // Show setup flow, not an error
    setScreen('onboarding')
} else {
    setScreen('main')
}
Enter fullscreen mode Exit fullscreen mode

There's a meaningful difference between "something went wrong" and "welcome, let's get you set up." The key-not-found state is expected on first launch — treat it that way.


The Verdict

API key storage is a trust signal. Users who see "stored securely in macOS Keychain" feel better about entering their key than users who don't know where it goes.

Two hours to implement properly. Worth it for every app that handles user credentials.


If this was useful, a ❤️ helps more than you'd think!

👉 HiyokoBar → https://hiyokomtp.lemonsqueezy.com/checkout/buy/f9b85321-6878-40aa-a472-ff748d6de2d5

X → @hiyoyok

Source: dev.to

arrow_back Back to Tutorials