Skip to content

Build your first app

Create, build and install a ShellCanvas app. An app is a single JSON package containing bundled JavaScript and CSS; the desktop loads it into an isolated frame and hands it typed services for the permissions you declared. No desktop rebuild, no restart.

Prerequisites

  • ShellCanvas installed. Installing app packages is supported on the Windows desktop.
  • Node.js 20 or newer for the SDK tooling.
  • A workspace to test in. A first app that only uses dialogs and storage needs no server at all, so the Local desktop workspace is enough.

Steps

1. Generate a project

sh
npx --package @techartdev/shellcanvas-app-sdk shellcanvas-app init my-notes \
  --id org.example.notes --title "My Notes"
cd my-notes
npm install

init refuses to write into an existing directory. It produces:

FilePurpose
shellcanvas.jsonThe manifest: id, version, title, permissions.
main.tsYour app's entry point.
style.cssStyles, bundled into the package.
package.jsonnpm run build (type-check, then package) and npm run check.
tsconfig.jsonStrict ES2022 + DOM, noEmit.

The generated manifest declares four permissions to start with: system.dialogs, system.storage, files.read and files.create. Remove what you do not use — users see and approve this list.

2. Write the app

Everything begins with one call. connectToShellCanvas() performs the handshake with the desktop and resolves to a client with the services you may use:

ts
import {
  connectToShellCanvas,
  RpcError,
} from "@techartdev/shellcanvas-app-sdk";

const client = await connectToShellCanvas();

// Tell the window chrome what to show.
await client.window.setDocumentState({
  dirty: false,
  busy: false,
  title: "My Notes",
});

// Keep the revision of the value this draft was loaded from.
const saved = await client.storage.get("note");
let revision = saved?.revision ?? null;
let savedText = typeof saved?.value === "string" ? saved.value : "";
let saving = false;

document.body.innerHTML = `
  <label for="note">Note</label>
  <textarea id="note"></textarea>
  <button id="save" type="button">Save</button>
  <p id="status" role="status"></p>`;
const box = document.querySelector<HTMLTextAreaElement>("#note")!;
const button = document.querySelector<HTMLButtonElement>("#save")!;
const status = document.querySelector<HTMLParagraphElement>("#status")!;
box.value = savedText;

const updateWindow = () =>
  client.window.setDocumentState({
    dirty: box.value !== savedText,
    busy: saving,
    title: "My Notes",
  });
box.addEventListener("input", () => {
  void updateWindow();
});
button.addEventListener("click", () => {
  void save();
});

async function save(): Promise<void> {
  if (saving) return;
  saving = true;
  button.disabled = true;
  const draft = box.value;
  status.textContent = "Saving…";
  try {
    await updateWindow();
    const committed = await client.storage.put("note", draft, revision);
    revision = committed.revision;
    savedText = draft;
    status.textContent =
      box.value === draft ? "Saved." : "Saved. New edits are still unsaved.";
  } catch (error) {
    status.textContent =
      error instanceof RpcError && error.code === "busy"
        ? "This note changed in another window. Your draft is intact. Copy it somewhere safe before reopening the note to reconcile the changes."
        : `Save failed: ${error instanceof Error ? error.message : String(error)}`;
  } finally {
    saving = false;
    button.disabled = false;
    await updateWindow();
  }
}

Keep the loaded revision until a save succeeds. Fetching a fresh revision just before writing an old draft would bypass conflict detection. Edits made while saving remain marked unsaved.

Two habits worth forming immediately:

  • Check before you offer. Ask client.services.list() or client.environment.get() and disable the parts of your interface whose services are unavailable, instead of calling and showing an error.
  • Handle RpcError. Every service failure is an RpcError with a code of invalid, closed, aborted, denied, unavailable, busy or failed. The message is written for people and can be shown as-is.

3. Build the package

sh
npm run build

This type-checks, bundles main.ts with esbuild, inlines style.css, validates the result and writes dist/app.shellcanvas.json. The bundle must be self-contained: an import left unresolved, or a second output file, fails the build. Validate any package explicitly with:

sh
npx shellcanvas-app validate dist/app.shellcanvas.json

4. Install it

In ShellCanvas, open App Manager → Add apps → Choose package… and select dist/app.shellcanvas.json. Review the identity, version and requested access, then choose Install app. Your app appears in the launcher and the dock.

To iterate: change the code, run npm run build again, and install the new file. Windows that are already open keep running the version they started with.

5. Share it from GitHub (optional)

sh
npx shellcanvas-app repository . --description "Quick notes for your hosts"

That writes shellcanvas.repo.json at the project root, containing the id, version, title, description and the SHA-256 of dist/app.shellcanvas.json. Commit it together with the built package, and add dist/app.shellcanvas.json -text to .gitattributes so the bytes are preserved exactly. Others install it with Add apps → From GitHub by entering owner/repository; ShellCanvas downloads the descriptor, downloads the package, checks the hash and shows the same review.

Expected result

dist/app.shellcanvas.json exists and validates, the app opens in its own window, and the state you store survives closing and reopening the window. A window title, dirty marker and busy state you set through client.window appear in the desktop's chrome.

Limitations

  • One bundle, no external assets. No runtime import(), no fetch to arbitrary URLs, no remote scripts, styles or fonts. The frame's policy blocks network access; use the network API for a user-configured endpoint.
  • The published SDK is 0.1.0 and provisional. Interfaces can change before 1.0. The --description flag and embedded icons need a newer SDK than the published one, and desktop 0.1.2 or later to install.
  • A declared permission is not a guarantee. It authorises a call; the service must also exist on the connection. Remote services need a connected workspace with the matching capability.
  • Installing apps is validated on Windows. Installed app interfaces are gated off on other native platforms.

ShellCanvas documentation