Creating a Store

Understand how to create persistent key-value stores with the Store plugin in Tauri v2, including store files, loading, and saving mechanisms.

A store in the Tauri Store plugin is a persistent, file‑backed key‑value container. It survives application restarts, can be accessed from both the webview (JavaScript/TypeScript) and Rust, and keeps data on disk in a simple JSON format. This page covers what store files are, how to create and load them, and the options for saving data.

Prerequisites

Before you create a store, the Store plugin must be installed and its permissions enabled in your Tauri capabilities file. If you haven’t added the plugin yet, follow the Store Plugin introduction or run the appropriate command in your project root:

npm run tauri add store
# or: pnpm tauri add store / yarn run tauri add store

Then make sure your capabilities configuration includes the required permission. Open src-tauri/capabilities/default.json and add "store:default" to the permissions array. Plugin permissions explains how those identifiers are scoped to windows.

{
  "permissions": [
    "core:default",
    "store:default"
  ]
}

What store:default covers:

The default permission set enables all store operations: load, get, set, delete, clear, save, and more. If you need more granular control later, you can replace it with specific allow-* permissions from the plugin’s permission table.

If you have already gone through the plugin installation guide, confirm that tauri-plugin-store is registered in your src-tauri/src/lib.rs. A typical setup looks like this:

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_store::Builder::default().build())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Store Files

Every store is backed by a single file on disk. You choose the file name when you create the store — for example, "settings.json" or "user_prefs.dat". The file is stored inside the application’s data directory (the app_data_dir), which Tauri resolves per platform:

  • Windows: C:\Users\<username>\AppData\Roaming\<bundle_identifier>
  • macOS: ~/Library/Application Support/<bundle_identifier>
  • Linux: ~/.local/share/<bundle_identifier>

The file format is JSON, so you can inspect or even hand‑edit it outside the app if needed. The plugin will create the file the first time data is saved — there is no manual file‑creation step.

Because the path is relative to the app data directory, you never need to worry about absolute paths or file system permissions. A store named "profile.json" will always live at the same predictable location for that application instance.

Simplified persistence:

Since the store path is just a file name, you can use the same name across platforms without any platform‑specific logic.

Loading Stores

There are two patterns for creating and loading a store instance in your frontend code: immediate loading with Store.load(), and deferred loading with LazyStore. Which one you choose depends on whether you need the data right away or are willing to pay the disk I/O cost later.

Store.load() reads the file from disk as soon as you call it, returning a ready‑to‑use Store instance. Use this when your component needs the stored data right at mount time.

import { Store } from '@tauri-apps/plugin-store';
async function loadStore() {
  // Loads (or creates) the store file 'config.json'
  const store = await Store.load('config.json', {
    autoSave: false,
  });
  // store is ready — you can call get/set now
  return store;
}

The options object lets you control behaviour like automatic saving. If a store with the same path has already been created anywhere in the app (even from Rust), the existing instance is returned and the options are ignored.

Loading and options:

If a store with the same path has already been created — for example, by another component or from Rust — the options you pass are silently ignored. The second Store.load() or new LazyStore() returns the existing instance. This is by design, but it can catch you off guard when you change autoSave and nothing seems to happen.

Accessing an Already‑Loaded Store from Rust

The store created in JavaScript can be accessed by Rust code that uses the same file path. The same resource‑table entry is shared, meaning any modification from either side is immediately visible to the other. On the Rust side you would use:

let store = app.store("config.json")?;
store.set("some-key", json!({"value": 5}));

This is particularly useful when you need to read or write persistent configuration from a Tauri command without re‑implementing the file handling.

Saving Stores

Stores give you fine‑grained control over when data is written to disk. Understanding the save lifecycle prevents data loss and avoids unnecessary disk I/O.

Manual Save

Call store.save() whenever you want to flush the in‑memory state to disk. This is the safest approach for critical data where you need to know exactly when persistence happens.

await store.set('preferences', { theme: 'dark' });
// Explicitly persist the change now
await store.save();

Auto‑Save

If you pass a number to the autoSave option during creation, the store will automatically write all changes to disk after a debounce delay. The number is the delay in milliseconds; if you omit it, the default is 100 ms.

const store = await Store.load('data.json', { autoSave: 300 });
await store.set('lastOpened', Date.now());
// 300 ms after the last set, the store saves automatically

autoSave and manual save together:

Setting autoSave to a number does not disable manual save(). You can still call store.save() at any time. However, if you’ve set a very short debounce, a manual save may race with the auto‑save — the second write will overwrite the first. This is rarely a real problem because both writes flush the same state, but it’s important to know that the disk I/O might happen twice.

Graceful Exit Save

Even with autoSave: false, the plugin attempts to persist all open stores when the application closes gracefully (for example, when the user quits normally). If the process crashes or is killed, unsaved changes are lost — just like any in‑memory data. Use explicit save() calls for mission‑critical data, and treat the graceful‑exit save as a safety net, not your primary persistence strategy.

Checkpoint:

If you’ve created a store, loaded it, and can see your data in the file after calling save(), everything is working correctly. The store file will contain the JSON you set.

A Complete React Example

Let’s tie all of this together with a small React component that creates a store, writes a value, reads it back, and provides a button to manually save. This example assumes you have a Tauri + Vite + React project set up and the Store plugin installed as shown earlier.

import { useState, useEffect } from 'react';
import { Store } from '@tauri-apps/plugin-store';
function App() {
  const [store, setStore] = useState<Store | null>(null);
  const [message, setMessage] = useState('');
  useEffect(() => {
    const init = async () => {
      // Load (or create) the store with auto‑save disabled
      const s = await Store.load('messages.json', { autoSave: false });
      setStore(s);
      // Retrieve a previously saved message, if any
      const saved = await s.get<string>('greeting');
      if (saved) {
        setMessage(saved);
      }
    };
    init();
  }, []);
  const handleSave = async () => {
    if (!store) return;
    await store.set('greeting', message);
    // Manually persist to disk
    await store.save();
    alert('Saved!');
  };
  return (
    <div>
      <h1>Store Example</h1>
      <input
        type="text"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
      />
      <button onClick={handleSave}>Save</button>
      <p>Message will be restored on next launch.</p>
    </div>
  );
}
export default App;

The component loads the store inside useEffect and reads any previously stored greeting. When the user clicks Save, the current input value is written and then explicitly persisted. On the next app launch, the saved greeting appears automatically.

Don’t forget to await:

Both store.set() and store.save() are asynchronous. If you call them without await, the operation is queued but you won’t know when it completes — and any code that runs immediately after may read stale data or close the app before the write finishes.

Missing permissions cause silent failures:

If the store operation seems to do nothing — no error, no data — verify that "store:default" (or the specific allow-* permissions) is present in your capabilities file. The plugin silently blocks commands for which permission is not granted.

Summary

The Store plugin turns a simple JSON file into a cross‑platform persistent key‑value store. By choosing between immediate loading (Store.load()) and deferred loading (LazyStore), you balance startup speed against instant data availability. Saving can be automatic, manual, or a mix of both — pick the approach that matches your data’s importance.