Storage
Plugin-scoped persistent storage. Keys are namespaced by plugin id in the main process. Requires the storage permission.
Storage-backed store (cross-webview sync)
Separate plugin webviews do not share memory. When one surface writes hc.storage and another needs to react (for example a sidebar and a modal overlay), reload from storage on focus/visibility instead of duplicating read/diff/notify logic in every plugin.
@harborclient/sdk/store provides:
createStorageStore<T>({ storage, key, parse, equals?, keepCurrentWhenMissing? })— returns{ subscribe, getSnapshot, useValue, reloadFromStorage, set }. Hydrates from storage asynchronously on creation; synchronousgetSnapshot()may showparse(undefined)until hydration completes.parsevalidates raw storage into a typed snapshot;setupdates memory and persists (write-through). Default equality usesJSON.stringify; pass a customequalsfor cheaper comparisons. SetkeepCurrentWhenMissing: truewhen an absent storage key should not reset in-memory state. AwaitreloadFromStorage()when you need the persisted value before a synchronous read.syncOnWindowFocus(stores, { intervalMs? })— wiresfocus,visibilitychange, and optional polling toreloadFromStorageon one or more stores. Returns aDisposable; dispose fromdeactivate()or a React effect cleanup.
import type { Disposable, PluginContext } from '@harborclient/sdk';
import { createStorageStore, syncOnWindowFocus } from '@harborclient/sdk/store';
let schemasStore: ReturnType<typeof createStorageStore<unknown[]>>;
let focusSync: Disposable | undefined;
export function activate(hc: PluginContext) {
schemasStore = createStorageStore({
storage: hc.storage,
key: 'schemas',
parse: (raw) => (Array.isArray(raw) ? raw : [])
});
focusSync = syncOnWindowFocus(schemasStore);
}
export function deactivate() {
focusSync?.dispose();
}
// In a component:
const schemas = schemasStore.useValue();
await schemasStore.set([...schemas, newEntry]);Use createExternalStore from the same module for in-webview-only state that does not need persistence.
Main entry
Optional main entry modules can use the same storage API. Plugin-scoped persistent key-value storage. Keys are namespaced by plugin id in the HarborClient main process (SQLite plugin_storage table). Requires the storage permission.
The main entry uses the same get / set API as the renderer. Calls from the SES utilityProcess child are routed to the Electron main process, which reads and writes through the same backing store as renderer hc.storage — no custom IPC channel or renderer bridge is required.
Values must be JSON-serializable. There is no delete or list API; store structured data under a few keys when you need collections or indexes.
Use main-process storage when HTTP hooks or other main-entry logic must persist state (OAuth tokens, API key config mirrored for onBeforeSend, response baselines, and similar). Renderer-only settings can stay in the renderer entry.
hc.storage.get(key)
Available since v2.0.0
Signature:<T>(key: string) => Promise<T | undefined>
Returns the stored value, or undefined if the key has never been set.
import type { MainPluginContext } from '@harborclient/sdk';
export function activate(hc: MainPluginContext): void {
hc.http.onBeforeSend(async (request) => {
const token = await hc.storage.get<string>('accessToken');
if (token) {
request.headers.Authorization = `Bearer ${token}`;
}
});
}hc.storage.set(key, value)
Available since v2.0.0
Signature:<T>(key: string, value: T) => Promise<void>
Persists a JSON-serializable value.
Invalid JSON already stored for a key causes get to throw with a plugin-scoped error message (same behavior as the renderer API).
await hc.storage.set('accessToken', refreshedToken);
await hc.storage.set('links', [{ collectionId: 1, dotenvPath: '/path/to/.env' }]);