Skip to content

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

ts
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.

LimitValue
Key length1–256 characters, no NUL
Value size1 Mi UTF-16 units of JSON per key
Namespace quota256 keys and 16 Mi units
list page size1–200 keys, default 100

Examples

Read, change, write, handling the conflict honestly:

ts
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:

ts
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:

ts
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:

ts
const existing = await client.storage.get("draft");
if (existing) await client.storage.remove("draft", existing.revision);

Errors

CodeMessageMeaning
invalidStorage keys must contain 1–256 characters without NUL.Bad key.
invalidSupply the last read revision, or null to create a value.Malformed revision.
invalidStore 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.
invalidList between 1 and 200 keys per page.Bad limit.
busyThis value changed in another window or app version. Read it again before saving.Revision conflict — expected, not exceptional.
unavailableThis app storage namespace reached its 256-key or 16 Mi unit quota.Remove keys, or store less.
unavailableApp storage could not be opened. Existing data has not been replaced.Local storage is unavailable; nothing was lost.
failedApp storage could not commit. Check available disk space and storage access.The write did not happen.
failedThe 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.

ShellCanvas documentation