Skip to main content
Desktop applications frequently need to store sensitive values — passwords, API tokens, OAuth refresh tokens — alongside ordinary configuration. Writing secrets to disk in a config file risks exposing them to other processes, log files, and backups. tauri-plugin-configurate routes these values through the OS keyring instead, keeping secrets off disk entirely.

What the OS keyring provides

The OS keyring is a platform-managed, access-controlled credential store. It survives application reinstalls, is protected by the user’s login session, and is not included in simple file-system copies of the application data directory.
Windows Credential Manager (wincred) stores credentials as “generic credentials” scoped to the current user. Credentials are encrypted at rest by the OS using DPAPI tied to the user’s login.

Marking fields with keyring()

Call keyring(typeCtor, { id }) in your schema to declare that a field is keyring-protected. The field will not appear in any written config file — only the config’s other fields are persisted to disk.
import { defineConfig, keyring } from "tauri-plugin-configurate-api";

const schema = defineConfig({
  theme: String,
  database: {
    host: String,
    // "password" is stored in the OS keyring — it never touches disk
    password: keyring(String, { id: "db-password" }),
  },
});
keyring() fields can appear at any depth in the schema, including inside nested objects and arrays. Each keyring() call must have a unique id across the entire schema.

KeyringOptions

Nearly every operation that involves keyring fields requires a KeyringOptions object:
interface KeyringOptions {
  service: string; // identifies your app in the OS keyring (e.g. "my-app")
  account: string; // identifies the "user" or namespace (e.g. "default")
}
service and account are used to construct the keyring lookup path. Use a consistent value throughout your app — changing either value makes previously stored secrets unreadable.
const KEYRING: KeyringOptions = {
  service: "my-tauri-app",
  account: "default",
};

Keyring storage path

Each keyring() field is stored at the OS keyring path:
service: {KeyringOptions.service}
account: {KeyringOptions.account}/{id}
For example, { service: "my-app", account: "default" } with a field keyring(String, { id: "db-password" }) is stored with account default/db-password under service my-app. For array keyring fields, the account is extended with the element’s dot-path index to keep each element distinct:
account: {account}/{id}::{encodedDotpath}
For example, the second element of a tokens array field with id: "token" stored under account default becomes default/token::tokens.1.
The id must not contain / because / is used as a separator in the account string. defineConfig() validates this at runtime and throws if a / is found in any id.

Locked vs unlocked states

When you load or save a config that has keyring() fields, the plugin distinguishes between two states: locked and unlocked.

Locked state

In the locked state, keyring fields are represented as null in the returned data. This is the default when you call .run() without chaining .lock() or .unlock().
// Load without unlocking — keyring fields come back as null
const locked = await config.load().run();
console.log(locked.data.database.password); // null

// TypeScript type: InferLocked<typeof schema>
// { theme: string; database: { host: string; password: null } }
LockedConfig<S> is the class wrapping this state. Its .data property is typed as InferLocked<S> — all keyring() fields are typed as null.

Unlocked state

In the unlocked state, keyring fields are populated with their actual values retrieved from the OS keyring. You reach this state by chaining .unlock(keyringOpts) instead of .run(), or by calling .unlock(keyringOpts) on an existing LockedConfig.
// Load with unlocking — keyring fields are populated from the OS keyring
const unlocked = await config.load().unlock(KEYRING);
console.log(unlocked.data.database.password); // "secret"

// Unlock from an existing locked result
const unlocked2 = await locked.unlock(KEYRING);
UnlockedConfig<S> is the class wrapping this state. Its .data property is typed as InferUnlocked<S>keyring() fields have their real TypeScript types.

Revoking access

Once you are done with unlocked data, call .lock() on the UnlockedConfig instance to revoke API-level access to the decrypted values:
const unlocked = await config.load().unlock(KEYRING);
const password = unlocked.data.database.password; // read the value

unlocked.lock(); // revoke access
unlocked.data;   // throws: "Cannot access data after lock() has been called."
.lock() is an API-level access guard, not a cryptographic wipe. JavaScript’s garbage collector manages memory reclamation, so the underlying buffer is not guaranteed to be zeroed immediately. Do not rely on .lock() as a memory-security primitive.

.lock() and .unlock() on operations

The .lock() and .unlock() builder methods on operation entries (LazyConfigEntry, LazyPatchEntry, LazyResetEntry) control how the plugin handles keyring fields during write operations.

Writing with .lock()

Chain .lock(keyringOpts) before .run() to store keyring fields in the OS keyring when creating or saving:
await config
  .create({
    theme: "dark",
    database: { host: "localhost", password: "secret" },
  })
  .lock(KEYRING)  // routes "password" to the OS keyring
  .run();
If the schema has keyring() fields and you call .run() without .lock(), the plugin throws an error:
Error: schema contains keyring fields — use .lock(opts) before .run(), or .unlock(opts), for create/save operations.

Writing and reading back with .unlock()

Replace .lock(...).run() with .unlock(keyringOpts) to both write to the keyring and immediately read back the unlocked data in one step:
const result = await config
  .create({ theme: "dark", database: { host: "localhost", password: "secret" } })
  .unlock(KEYRING);

console.log(result.data.database.password); // "secret"

Deleting keyring entries

When you call config.delete(keyringOpts), the plugin deletes the config file and also removes all associated keyring entries from the OS keyring:
await config.delete(KEYRING); // removes file + clears keyring entries
Omit the argument to skip keyring cleanup (leaving orphaned entries in the OS keyring):
await config.delete(); // removes file only

Build docs developers (and LLMs) love