Key-Value Store
Hermes includes a built-in key-value store for persisting small amounts of typed data across application launches. It is JSON-backed, thread-safe, and works on all platforms without any external dependencies.
Why Not localStorage?
WebView-hosted apps can use the browser's localStorage, but it has a fundamental gap for tray apps and multi-window scenarios: the data is tied to the webview instance. When a tray window is torn down between sessions, or when multiple windows need to share state, localStorage values are lost or siloed. HermesStore lives on the .NET side, so data survives regardless of webview lifecycle.
Quick Reference
using Hermes.Storage;
// Use the default store
HermesStore.Default.Set("theme", "dark");
var theme = HermesStore.Default.Get<string>("theme"); // "dark"
// Use a named store
var settings = HermesStore.Open("settings");
settings.Set("volume", 80);
settings.Set("lastOpened", DateTime.UtcNow);Getting and Setting Values
Values can be any JSON-serializable type — primitives, records, collections, or nested objects.
// Primitives
HermesStore.Default.Set("count", 42);
HermesStore.Default.Set("enabled", true);
// Complex types
HermesStore.Default.Set("user", new UserPrefs("dark", 14));
var prefs = HermesStore.Default.Get<UserPrefs>("user");
// Collections
HermesStore.Default.Set("recent", new List<string> { "a.txt", "b.txt" });
var recent = HermesStore.Default.Get<List<string>>("recent");Safe Retrieval
Get<T> returns default(T) if the key is missing or the stored value can't be deserialized to T. If you need to distinguish "missing" from "stored null", use TryGet:
// Returns default(T) on miss or type mismatch
var count = HermesStore.Default.Get<int>("count"); // 0 if missing
var name = HermesStore.Default.Get<string>("name", "N/A"); // "N/A" if missing
// Explicit success/failure
if (HermesStore.Default.TryGet<int>("count", out var value))
{
Console.WriteLine($"Count is {value}");
}Deserialization failures are logged as warnings but never thrown — one corrupted key won't affect the rest of the store.
Other Operations
// Check for a key
if (HermesStore.Default.Contains("theme")) { /* ... */ }
// Enumerate all keys
foreach (var key in HermesStore.Default.Keys) { /* ... */ }
// Remove a single key
HermesStore.Default.Remove("theme");
// Clear everything
HermesStore.Default.Clear();Named Stores
HermesStore.Open(name) returns an isolated store backed by its own JSON file. The same name always returns the same instance (cached internally), so you can call Open freely without worrying about duplicate file handles.
var session = HermesStore.Open("session");
var cache = HermesStore.Open("cache");
session.Set("token", "abc123");
cache.Set("lastSync", DateTime.UtcNow);Store names must be 1-64 characters and may contain letters, digits, ., -, and _. Reserved names like CON, PRN, and COM1 are rejected to avoid issues on Windows.
Persistence
Every Set, Remove, and Clear call writes to disk immediately — there is no explicit save step. Writes are atomic (temp file + rename) to prevent corruption on crash.
Data is stored as JSON in the platform's standard user-data directory:
| Platform | Directory |
|---|---|
| macOS | ~/Library/Application Support/Hermes/KvStore/ |
| Windows | %LOCALAPPDATA%\Hermes\KvStore\ |
| Linux | $XDG_DATA_HOME/Hermes/KvStore/ (defaults to ~/.local/share/Hermes/KvStore/) |
Each named store is a separate file ({name}.json). The default store is default.json.
Inspecting stored data
The JSON files are human-readable. You can open them in any text editor to inspect or manually edit stored values during development.
Thread Safety
All operations on a store instance are thread-safe. Reads and writes are serialized internally, so you can call Get and Set from any thread — including background tasks, event handlers, and webview message callbacks — without additional locking.
Multi-process access
The store is designed for single-process use. If your application uses multiple processes that write to the same store, pair it with SingleInstanceGuard to ensure only one process is active.
Blazor Integration
Register the store in your Blazor app's service container, then inject it into components:
// In Program.cs or your app builder
builder.Services.AddKeyValueStore(); // registers the default store
builder.Services.AddKeyValueStore("settings"); // registers a named store@using Hermes.Storage
@inject IHermesKeyValueStore Store
<button @onclick="SavePreference">Save</button>
@code {
private void SavePreference()
{
Store.Set("sidebar", "collapsed");
}
}Example: Persisting Favorites in a Tray App
This pattern is used in the PokedexTray sample. The tray window is torn down between sessions, so localStorage would lose the user's favorites list. The KV store keeps it on the .NET side.
using Hermes.Storage;
// Define keys
private const string LastViewedKey = "lastViewedId";
private const string FavoritesKey = "favorites";
// On page init — replay persisted state
var lastViewed = HermesStore.Default.Get<int?>(LastViewedKey);
var favorites = HermesStore.Default.Get<List<Favorite>>(FavoritesKey)
?? new List<Favorite>();
// On lookup — remember what the user viewed last
HermesStore.Default.Set(LastViewedKey, id);
// On toggle favorite — update and persist the list
var favorites = HermesStore.Default.Get<List<Favorite>>(FavoritesKey)
?? new List<Favorite>();
var index = favorites.FindIndex(f => f.Id == id);
if (index >= 0)
favorites.RemoveAt(index);
else
favorites.Add(new Favorite(id, name));
HermesStore.Default.Set(FavoritesKey, favorites);public record Favorite(int Id, string Name);The next time the app launches, Get<List<Favorite>>(FavoritesKey) returns the full list immediately — no webview needed.
Next Steps
- Tray Applications — the PokedexTray sample demonstrates this pattern end-to-end
- WebView Interop — sending persisted state to the frontend via
SendMessage - Single Instance — ensure only one process accesses the store
