App lifecycle and services
How an app connects to ShellCanvas, what it is allowed to reach, how failures are reported, and what it must clean up. Read this once and the individual API pages will make sense on their own.
How it works
The handshake
An app runs in a sandboxed frame with no same-origin privileges, no network access and a strict content policy. It reaches the desktop through exactly one channel, opened by connectToShellCanvas():
import { connectToShellCanvas } from "@techartdev/shellcanvas-app-sdk";
const client = await connectToShellCanvas(); // default timeout: 10 secondsThe SDK reads the instance token the desktop injected into the document, announces itself to the host frame, and waits for a message port. Three failures are possible, and they are plain Errors rather than service errors:
| Message | Meaning |
|---|---|
Missing ShellCanvas instance identity. | The document was not produced by ShellCanvas. |
Launch this app inside ShellCanvas. | The page is not running inside the desktop's frame. |
ShellCanvas did not establish an app channel. | The desktop did not answer within the timeout. |
What the client gives you
The resolved client is a set of frozen service objects:
| Service | Page |
|---|---|
client.files | Files API |
client.transfers | Transfers API |
client.console | Console API |
client.storage, client.settings | Storage API |
client.network | Network and credentials API |
client.clipboard | Clipboard API |
client.window | Window API |
client.system.dialogs, client.system.files | System dialogs API |
client.environment, client.events, client.services | Events and environment API |
client.hostSettings | Host settings API |
client.storage and client.settings are the same interface over two separate namespaces, so configuration and data do not collide.
Permissions and availability are different things
A call succeeds only when both hold:
- The permission is granted. Effective grants are the intersection of your manifest's
permissionsand what the user approved. Otherwise:denied— The extension does not hold the required permission. - The service is available. Remote services need a connected workspace whose provider offers the matching capability. Otherwise:
unavailable— This service is currently unavailable.
Ask before you offer, rather than calling and reporting failure:
const methods = await client.services.list();
const canEdit = methods.some((m) => m.name === "system.files.saveText" && m.granted && m.available);
saveButton.disabled = !canEdit;Some services need no permission at all, because the authority is intrinsic: the window API acts only on your own window, and discovery, environment and events describe what you already have.
Error model
Every service rejection is an RpcError with a code:
| Code | Meaning | Typical response |
|---|---|---|
invalid | Malformed parameters. | A bug in the app; fix the call. |
denied | Permission not granted. | Hide or disable the feature. |
unavailable | Service missing on this connection. | Explain, and offer it again when the environment changes. |
busy | Concurrency limit, or a revision conflict. | Wait, or re-read and retry. |
aborted | Cancelled by signal or shutdown. | Usually silent. |
closed | The channel, handle or binding ended. | Re-acquire the handle. |
failed | The provider or host failed. | Show the message; it is written for people. |
Messages are user-readable and can be displayed directly. Cancellation deserves care: an aborted write may still have been delivered — the message says so — so inspect before retrying blindly.
Protocol limits
One channel carries every call, so it has guard rails: at most 64 requests in flight per window (busy, Too many pending requests.), messages up to 4 Mi UTF-16 units (invalid), and JSON nesting up to 64 levels. Bulk data is streamed by the service that owns it — file listings page, consoles chunk, clipboard and network responses read in pieces — rather than being sent in one message.
Instances, updates and the workspace
- One window is one instance, with its own client, its own handles and its own limits. Opening your app twice gives two independent instances.
- A running window is pinned to the package version and grants it started with. Installing an update does not change it; the next window uses the new version.
- Minimizing keeps the instance; closing ends it and releases everything the host held for it.
- Switching workspaces keeps your instance alive. The binding it holds may change, and handles from the previous connection reject with
closedrather than acting on the wrong machine. Subscribe to environment events and re-acquire.
Cleanup
Disposal happens automatically when the window closes, and once on page hide. What you must release yourself while running:
| Handle | Release with |
|---|---|
RemoteConsole | close() |
RemoteTransfer | close() |
AppHTTPResponse | close(), ideally in finally |
files.list(...) iterator | Break or return from the loop |
events.subscribe(...) | Call the returned unsubscribe function |
client.dispose() closes the channel: pending calls reject with closed, and the desktop releases the rest.
Boundaries
- No escape hatches. The frame has no network access, no Node APIs, no direct desktop IPC, and no browser clipboard.
client.call()reaches the same explicit method map as the typed services, with the same permission checks. - Secrets stay outside the app. API keys configured through the network API are never returned to it. Local download paths chosen by the user are not exposed.
- Handles are owned by the window that created them. They cannot be shared between instances and do not survive their owner.
- Cancellation is a request, not a guarantee. A cancelled transfer may still complete; a cancelled write may already have been delivered.
- Concurrency is bounded per window: 16 directory listings, 16 consoles, 32 prepared transfers, 4 network requests, one clipboard read and one write, one outstanding host-settings operation. Exceeding a limit fails with
busyrather than queueing indefinitely.
Related guides
- Build your first app — the smallest working app.
- Events and environment API — reacting to connection and capability changes.
- Manifests and packaging — declaring identity and permissions.
- Trust and isolation model — what the isolation does and does not claim.