Secure storage
How secure storage works
Section titled “How secure storage works”gpui-starter stores secrets (API tokens, passwords, signing keys) in the operating system credential store, not in SQLite or log files. The secure_storage module at src/services/secure_storage.rs wraps the keyring crate (v3.6.3) and exposes four functions: initialize, snapshot, set_secret, get_secret, and delete_secret.
The database only holds opaque references (a service name and key pair). The actual secret values live in the platform keyring. If you are new to the project, start with the getting started guide to get the app running, then come back here.
Platform credential stores
Section titled “Platform credential stores”The keyring crate delegates to the native credential manager on each platform. No extra configuration is needed; the correct backend is selected at compile time.
| Platform | Backend |
|---|---|
| macOS | Keychain Services (via apple-native feature) |
| Linux | Secret Service API (GNOME Keyring, KDE Wallet) |
| Windows | Credential Manager |
On Linux, the user must have a keyring daemon running (GNOME Keyring or KDE Wallet). If no daemon is available, the availability check will fail and the module degrades gracefully.
Availability check and degraded mode
Section titled “Availability check and degraded mode”On startup, secure_storage::initialize(cx) probes the keyring by creating a test entry:
let available = keyring::Entry::new("gpui-starter", "availability-check").is_ok();If the probe fails, the module registers itself as degraded in the capability registry. The global SecureStorageSnapshot tracks whether the keyring is reachable and records the last error. You can check it at any point:
let snap = secure_storage::snapshot(cx);if !snap.available { // keyring is unreachable, show degraded UI}The capability registry entry ("secure_storage") exposes supported, enabled, degraded, and last_error fields. The diagnostics view reads these fields to surface keyring problems to the user. See the architecture doc for how the capability registry fits into the broader state model.
API reference
Section titled “API reference”All functions that mutate state take &mut App so they can update the global error state.
initialize
Section titled “initialize”pub fn initialize(cx: &mut App)Called once during app startup. Probes the keyring, sets the global SecureStorageSnapshot, and registers the capability. Does not return a value. If the keyring probe fails, the snapshot records the error and the capability is marked degraded.
snapshot
Section titled “snapshot”pub fn snapshot(cx: &App) -> SecureStorageSnapshotReturns a clone of the current keyring state. Available at any time after initialization. Returns a default (unavailable) snapshot if initialize has not been called.
set_secret
Section titled “set_secret”pub fn set_secret( service: &str, key: &str, value: &str, cx: &mut App,) -> Result<(), SecureStorageError>Writes a secret under the given service and key. On success, clears last_error and returns Ok(()). On failure, logs through tracing and returns a SecureStorageError variant with the service, key, and original keyring::Error.
get_secret
Section titled “get_secret”pub fn get_secret( service: &str, key: &str, cx: &mut App,) -> Result<Option<SharedString>, SecureStorageError>Returns Some(value) if the entry exists. Returns None if the entry does not exist (keyring::Error::NoEntry). Returns an error on read failure (keyring unreachable, permission denied, etc.).
delete_secret
Section titled “delete_secret”pub fn delete_secret( service: &str, key: &str, cx: &mut App,) -> Result<(), SecureStorageError>Deletes the credential from the OS keyring. Fails if the entry does not exist or the keyring is unreachable. Clears last_error on success.
Error types
Section titled “Error types”The module uses a typed error enum rather than string errors. Each variant carries the service and key that caused the failure, plus the original keyring::Error as the source:
pub enum SecureStorageError { EntryCreation { service: String, key: String, source: keyring::Error }, SetFailed { service: String, key: String, source: keyring::Error }, GetFailed { service: String, key: String, source: keyring::Error }, DeleteFailed { service: String, key: String, source: keyring::Error },}This lets callers match on specific failure modes and display targeted UI messages. The error types also integrate with the notification system described in the notifications doc.
The reference pattern: opaque IDs in SQLite
Section titled “The reference pattern: opaque IDs in SQLite”The architecture separates secret values from structured data. The database stores references; the keyring holds the values.
-- WRONG: secret value in the databaseINSERT INTO environments (name, api_token) VALUES ('prod', 'sk-abc123');
-- CORRECT: opaque reference in the databaseINSERT INTO secret_refs (id, service, key) VALUES (?, 'gpui-starter', 'prod-api-token');-- actual value lives in the OS keyring under service='gpui-starter', key='prod-api-token'To read a secret at runtime, look up the reference row from SQLite, then call get_secret with the stored service and key. This pattern is also documented in the architecture guide under the “Secrets” section.
Using secrets in a view
Section titled “Using secrets in a view”A complete example of writing and reading a secret from a settings page, with error handling wired to the notification system:
use crate::secure_storage;use crate::notifications;
fn save_api_token(token: &str, window: &mut Window, cx: &mut Context<SettingsView>) { let message = match secure_storage::set_secret( "gpui-starter", "api-token", token, cx, ) { Ok(()) => "API token saved to keyring".to_string(), Err(err) => format!("Failed to save token: {err}"), }; notifications::push(message, window, cx);}
fn load_api_token(cx: &mut Context<SettingsView>) -> Option<String> { secure_storage::get_secret("gpui-starter", "api-token", cx) .ok() .flatten() .map(|s| s.to_string())}The set_secret call updates the global last_error field automatically, so the diagnostics page reflects keyring health without any extra wiring from the caller.
Logging policy
Section titled “Logging policy”Every function logs through tracing with the target gpui_starter::secure_storage. The log messages include the service and key fields but never the secret value itself:
tracing::info!( target: "gpui_starter::secure_storage", service, key, "secret written");If you add new code that touches secrets, follow the same rule: log the identifier, not the payload.
Testing without the OS keyring
Section titled “Testing without the OS keyring”The testing module provides FakeSecureStorage for unit tests that should not touch the OS keyring. It is a plain struct with no GPUI dependency, so it works in both #[test] and #[gpui::test] contexts. See the testing guide for the full test setup reference.
#[derive(Default)]pub struct FakeSecureStorage { value: Option<String>,}
impl FakeSecureStorage { pub fn set(&mut self, value: &str) { self.value = Some(value.to_string()); } pub fn get(&self) -> Option<String> { self.value.clone() } pub fn delete(&mut self) { self.value = None; }}Usage in a test:
#[test]fn fake_storage_roundtrip() { let mut storage = FakeSecureStorage::default(); storage.set("token"); assert_eq!(storage.get().as_deref(), Some("token")); storage.delete(); assert_eq!(storage.get(), None);}For tests that need the full GPUI context, you can set a fake global snapshot instead of probing the real keyring:
#[gpui::test]fn test_with_degraded_keyring(cx: &mut TestAppContext) { cx.update(|cx| { cx.set_global(SecureStorageSnapshot { available: false, last_error: Some("keyring entry initialization unavailable".into()), }); }); // code under test sees degraded state}Export and import: redacting secrets
Section titled “Export and import: redacting secrets”When exporting application data (telemetry config, environment snapshots), secrets must never appear in the output. The telemetry module demonstrates the redaction pattern:
fn redact_endpoint(endpoint: &str) -> Option<String> { let host = endpoint .split("://") .nth(1) .unwrap_or(endpoint) .split('/') .next() .unwrap_or(endpoint); Some(format!("{host}/..."))}Three rules for export/import flows:
- Replace secret values with placeholders or omit them entirely.
- Store only the service/key reference pair so the imported config can rebind secrets on the target machine.
- Never log raw secret values. The
tracingcalls insecure_storage.rslog the service and key, never the value.
Function reference
Section titled “Function reference”| Function | Returns | Failure mode |
|---|---|---|
initialize(cx) | Sets global SecureStorageSnapshot and capability | Marks degraded if keyring probe fails |
snapshot(cx) | SecureStorageSnapshot | Returns default if not initialized |
set_secret(service, key, value, cx) | Result<(), SecureStorageError> | Error on entry creation or write failure |
get_secret(service, key, cx) | Result<Option<SharedString>, SecureStorageError> | None if no entry, error on read failure |
delete_secret(service, key, cx) | Result<(), SecureStorageError> | Error if entry missing or keyring unreachable |