Storage API
client.storage and client.settings keep small JSON values on the user's computer, private to your app. They need no connection, so they work in the local workspace and while a host is offline.
Availability
Both need the system.storage permission. Nothing else: no capability, no connected workspace.
The two namespaces share one interface and one quota shape, but their contents are separate — keep documents and state in storage, and user preferences in settings, so a "reset settings" feature cannot delete data.
Interface
interface AppValue {
readonly revision: string;
readonly value: Json;
}
interface StoragePage {
readonly keys: readonly string[];
readonly next: string | null;
}
interface AppStorageAPI {
get(key: string, signal?: AbortSignal): Promise<AppValue | null>;
put(
key: string,
value: Json,
expectedRevision: string | null,
signal?: AbortSignal,
): Promise<AppValue>;
remove(
key: string,
expectedRevision: string | null,
signal?: AbortSignal,
): Promise<void>;
list(
options?: { after?: string; limit?: number },
signal?: AbortSignal,
): Promise<StoragePage>;
}Every write is a compare-and-set. Pass the revision you last read, or null to create a key that must not exist yet. A mismatch fails with busy instead of overwriting, which is what makes two windows of the same app safe.
| Limit | Value |
|---|---|
| Key length | 1–256 characters, no NUL |
| Value size | 1 Mi UTF-16 units of JSON per key |
| Namespace quota | 256 keys and 16 Mi units |
list page size | 1–200 keys, default 100 |
Examples
Read, change, write, handling the conflict honestly:
import { RpcError } from "@techartdev/shellcanvas-app-sdk";
async function saveNote(
text: string,
draftRevision: string | null,
): Promise<string | null> {
try {
const committed = await client.storage.put("note", text, draftRevision);
return committed.revision;
} catch (error) {
if (error instanceof RpcError && error.code === "busy") {
const fresh = await client.storage.get("note");
await showConflict(text, fresh?.value); // let the user decide
return null;
}
throw error;
}
}Pass the revision loaded with the draft, not a newly fetched revision. After success, keep the returned revision alongside the saved text. On conflict, preserve the draft and reconcile it with the newer value before attempting another save.
Create a key only if it does not exist:
try {
await client.settings.put("theme", { accent: "blue" }, null);
} catch (error) {
if (!(error instanceof RpcError && error.code === "busy")) throw error;
// Already configured; leave the user's choice alone.
}Walk every key:
let after: string | undefined;
do {
const page = await client.storage.list({ after, limit: 200 });
for (const key of page.keys) console.log(key);
after = page.next ?? undefined;
} while (after);Delete with the same optimistic check:
const existing = await client.storage.get("draft");
if (existing) await client.storage.remove("draft", existing.revision);Errors
| Code | Message | Meaning |
|---|---|---|
invalid | Storage keys must contain 1–256 characters without NUL. | Bad key. |
invalid | Supply the last read revision, or null to create a value. | Malformed revision. |
invalid | Store at most 1 Mi UTF-16 units per value; use file services for documents and bulk data. | The value is too big; keep large content in files. |
invalid | List between 1 and 200 keys per page. | Bad limit. |
busy | This value changed in another window or app version. Read it again before saving. | Revision conflict — expected, not exceptional. |
unavailable | This app storage namespace reached its 256-key or 16 Mi unit quota. | Remove keys, or store less. |
unavailable | App storage could not be opened. Existing data has not been replaced. | Local storage is unavailable; nothing was lost. |
failed | App storage could not commit. Check available disk space and storage access. | The write did not happen. |
failed | The saved app value is invalid. It has been preserved. | A stored value could not be parsed; it is kept, not deleted. |
Lifecycle and permissions
Storage belongs to the installation, not to a window or a workspace. All windows of your app share it, and it is unaffected by which host is connected.
Updating your app keeps its data. Removing and reinstalling does not: the desktop issues a new installation identity, and the new installation starts empty. The old records may remain on disk until storage cleanup; retiring their identity is not physical deletion. Tell users that reinstalling will not recover access to their old data, and offer an export if that matters.
Values are stored locally on the user's computer, never on a remote host and never synchronised between devices. They are not a secure store: do not keep passwords or API keys here — use the network API, which keeps credentials outside your app entirely.
Related guides
- Network and credentials API — for anything secret.
- Files API — for documents and bulk data.
- Manifests and packaging — identity, updates and what a reinstall resets.
- App lifecycle and services — error codes and window model.