Skip to main content
The keyring integration lets you keep sensitive values — API keys, passwords, tokens — out of your config files entirely. Instead of writing secrets to disk alongside your other settings, the plugin stores them in the platform’s native secret store: macOS Keychain, Windows Credential Manager, or Linux Secret Service / Keyutils. Your config file on disk holds only non-sensitive fields; the secrets live in the OS and are retrieved on demand.

Declaring keyring fields

Use keyring() in your schema to mark a field as keyring-protected. You must give each field a unique id string — this becomes part of the OS keyring entry identifier.
import { defineConfig, keyring, optional } from "tauri-plugin-configurate-api";

const schema = defineConfig({
  theme: String,
  apiKey: keyring(String, { id: "api-key" }),
});
See the Schema page for the full rules governing keyring() ids.

KeyringOptions

Most keyring operations accept a KeyringOptions object that identifies which keyring entry to use:
interface KeyringOptions {
  service: string;  // Keyring service name (e.g. your app name)
  account: string;  // Keyring account name (e.g. "default")
}
Both service and account must be non-empty, non-whitespace strings without control characters. Use a consistent pair throughout your app so that all operations address the same keyring entries.

Writing with .lock()

When you create or save a config that contains keyring fields, chain .lock(keyringOpts) before .run(). The plugin separates the secret values from the rest of the data, stores them in the OS keyring under {account}/{id}, and writes only the non-secret fields to the config file.
import {
  BaseDirectory,
  Configurate,
  JsonProvider,
  defineConfig,
  keyring,
} from "tauri-plugin-configurate-api";

const schema = defineConfig({
  theme: String,
  apiKey: keyring(String, { id: "api-key" }),
});

const config = new Configurate({
  schema,
  fileName: "app.json",
  baseDir: BaseDirectory.AppConfig,
  provider: JsonProvider(),
});

const KEYRING = { service: "my-app", account: "default" };

// Write — stores apiKey to the OS keyring; theme goes into app.json
await config.create({ theme: "dark", apiKey: "sk-abc123" }).lock(KEYRING).run();

Reading with .unlock()

When you load a config that has keyring fields, chain .unlock(keyringOpts) to retrieve the secrets from the OS keyring and merge them back into the data object. The result is an UnlockedConfig instance whose .data property has keyring fields populated with their real values.
.unlock() requires the configurate:allow-unlock permission, which is not included in configurate:default. You must add it explicitly to your capability file. See the Permissions page for details.
// Read with secrets — requires configurate:allow-unlock permission
const { data } = await config.load().unlock(KEYRING);
console.log(data.apiKey); // "sk-abc123"

Locked vs unlocked data

When you call .run() on a load (or skip .unlock()), you receive a LockedConfig instance. Keyring fields in LockedConfig.data are null — the secrets have not been retrieved from the OS.
// Read without secrets — no configurate:allow-unlock permission needed
const locked = await config.load().run();
console.log(locked.data.apiKey); // null
The TypeScript types reflect this distinction automatically. InferLocked<S> gives keyring fields the type null; InferUnlocked<S> gives them their real types (e.g. string).

Unlocking a LockedConfig after the fact

If you already have a LockedConfig and want to retrieve secrets later, call .unlock() directly on it:
const locked = await config.load().run();

// ... do something with non-secret fields ...
console.log(locked.data.theme); // "dark"

// Unlock later when secrets are needed
const unlocked = await locked.unlock(KEYRING);
console.log(unlocked.data.apiKey); // "sk-abc123"

Revoking access with UnlockedConfig.lock()

UnlockedConfig exposes a .lock() method that revokes access to the decrypted data through that instance. After calling it, any access to .data throws an error. Use this to reduce the window during which secrets are accessible in memory.
UnlockedConfig.lock() is an API-level access guard, not a cryptographic memory wipe. JavaScript’s garbage collector controls when the underlying memory is actually reclaimed. Treat it as a structural safeguard to prevent accidental use of stale secret data, not as a guarantee of immediate memory clearing.
const unlocked = await config.load().unlock(KEYRING);
console.log(unlocked.data.apiKey); // "sk-abc123"

unlocked.lock(); // revoke access

console.log(unlocked.data.apiKey); // throws: "Cannot access data after lock() has been called. Load or unlock again."

Complete example

const schema = defineConfig({
  theme: String,
  apiKey: keyring(String, { id: "api-key" }),
});

const config = new Configurate({
  schema,
  fileName: "app.json",
  baseDir: BaseDirectory.AppConfig,
  provider: JsonProvider(),
});

const KEYRING = { service: "my-app", account: "default" };

// Write — stores apiKey to OS keyring
await config.create({ theme: "dark", apiKey: "sk-abc123" }).lock(KEYRING).run();

// Read with secrets
const { data } = await config.load().unlock(KEYRING);
console.log(data.apiKey); // "sk-abc123"

// Read without secrets
const locked = await config.load().run();
console.log(locked.data.apiKey); // null

// Unlock from a locked instance
const unlocked = await locked.unlock(KEYRING);
console.log(unlocked.data.apiKey); // "sk-abc123"

Relationship to BinaryProvider encryption

The keyring integration and BinaryProvider encryption are independent features. Keyring fields are schema-level annotations that route specific values to the OS secret store. BinaryProvider’s encryptionKey encrypts the entire config file at rest. You can use both together, but the plugin never stores a BinaryProvider encryption key in the OS keyring automatically — managing that key is your responsibility.