Skip to content

Packaging

Gadgets are distributed as .torchsnap archives. A .torchsnap file is a standard zip archive containing the gadget’s manifest, compiled WASM binary, and any additional files the gadget needs (frontend bundles, SQL migrations, bundled assets). This page covers what goes into an archive, how to create one, and how gadgets are loaded by the host.

A .torchsnap archive must contain a manifest.toml at its root. The host reads the manifest first and uses it to locate every other file in the archive. Beyond the manifest, the archive contains whichever files the manifest references:

File Manifest field Required
WASM binary [gadget] wasm Yes
Launcher JS bundle [frontend] launcher-bundle If views are declared
Launcher CSS [frontend] launcher-css Optional
Settings JS bundle [frontend] settings-bundle If settings panel is declared
Settings CSS [frontend] settings-css Optional
SQL migrations [storage.sql] migrations If SQL storage is used
Bundled assets (any path used with assets::read) Optional

Any additional files (data files, icon assets, templates) can be included and read at runtime through the assets import. The archive can contain arbitrary directory structure.

All file paths in the manifest and in runtime assets::read calls are validated by the host. The following are rejected:

  • Empty strings
  • Absolute paths (leading /)
  • Paths containing .. segments (even if they would resolve inside the root)
  • Backslashes (on all platforms)
  • NUL bytes
  • Windows drive letters (C:, z:)

This prevents path traversal attacks, which is important since third-party gadgets are untrusted code.

A .torchsnap archive is a zip file with manifest.toml at the root. To create one manually:

  1. Build the WASM binary (targeting wasm32-wasip2).
  2. Build the frontend bundles if your gadget has a UI (typically via Vite).
  3. Collect the manifest, WASM binary, frontend output, migration files, and any bundled assets into a directory.
  4. Zip the directory contents (not the directory itself) into a file named <gadget-id>.torchsnap.

The manifest’s [gadget] wasm field must match the WASM binary’s filename inside the archive. Cargo converts hyphens to underscores in crate names, so a crate named my-gadget produces my_gadget.wasm.

The archive should contain only the files the gadget needs at runtime, not the source code or build tooling used to produce them. The general rule: if the host or the gadget reads the file, it belongs in the archive. If it’s only needed during compilation, it doesn’t.

Include:

  • manifest.toml
  • The compiled .wasm binary (output of cargo build)
  • Frontend bundles (the built JS/CSS output from Vite, not the source)
  • SQL migration files
  • Bundled assets (icons, data files, templates)

Exclude:

  • Rust source (src/, Cargo.toml, Cargo.lock, target/)
  • Frontend source and tooling (frontend/src/, frontend/node_modules/, frontend/package.json, frontend/vite.config.ts, etc.)
  • Version control files (.git/, .gitignore)

The in-tree just package-gadget recipe applies this distinction automatically using a blacklist. Any new file you add to the gadget directory (migrations, assets, data files) is included by default without recipe changes.

During development, gadgets do not need to be packaged into archives. In debug builds, the host automatically discovers every directory under the gadgets/ workspace that contains a manifest.toml. This lets you iterate without creating a zip file on every change.

The typical development cycle is:

  1. Edit your gadget’s Rust source or frontend code.
  2. Rebuild with just build-gadget <gadget-id>.
  3. Restart the app to pick up the changes.

Development gadgets appear with a “Dev” badge in the Gadgets settings panel to distinguish them from installed gadgets.

Users install a gadget by opening its .torchsnap archive in Torchsnap: from Settings → Gadgets (file picker or drop zone), by double-clicking it in Finder, or by passing its path to the Torchsnap binary on the command line. On macOS, Torchsnap registers .torchsnap as its own document type (app.torchsnap.gadget, MIME type application/vnd.torchsnap.gadget+zip), so Finder opens these files with Torchsnap and shows them with a gadget icon. Installing Gadgets describes the process from the user’s side.

Torchsnap copies the archive before looking at it and rejects archives larger than 16 MiB. It then checks that the zip is valid, that manifest.toml is present and parseable, and that all referenced paths pass validation. The user sees a review of the gadget and every permission its manifest declares, and nothing is installed until they confirm it.

The gadget ID decides what an install does:

  • An ID that belongs to a built-in, bundled or development gadget is rejected.
  • An ID that is already installed as a user gadget is replaced. The gadget’s data and settings are kept, and the review shows which permissions are new.
  • A replace is rejected when the installed gadget was placed as a directory under the user gadgets directory, or when the new archive declares fewer entries in [storage.sql] migrations than the installed one, since the new version could not open the installed version’s database.

Users update your gadget by installing the archive of the new version over the installed one. To make that work smoothly:

  • Keep the gadget id. A different ID installs as a separate gadget with empty data and settings.
  • Raise the version. The review compares the two versions and warns the user when the archive is older than the installed one.
  • Only add migrations at the end of the list. An archive with fewer migrations than the installed version is refused.

The review marks permissions the installed version didn’t have as New and permissions you dropped as Removed, with a summary line above the list. Users see every added permission before they confirm the update.

In a debug build, the host loads every gadget under the gadgets/ workspace as a development gadget. Installing an archive with the same ID is rejected, so the review of your own gadget only shows the rejection. To try the install flow, run a release build (just build --release), which does not load development gadgets, and open your archive there.

Only user-installed gadgets can be uninstalled. The gadget keeps running until the next start and shows Removed on restart until then, and the user can undo the uninstall. When Torchsnap starts again, it deletes the archive, the gadget’s entire state directory (database, compile cache), and all settings keys in the gadget’s namespace, before any gadget loads. Gadgets bundled with the app cannot be uninstalled.

The host maintains two separate locations for every gadget: one for its read-only code and one for its writable runtime data. Understanding the distinction matters because your gadget can access both, but for different purposes.

The .torchsnap archive (or directory during development) contains everything you ship: the compiled WASM binary, frontend bundles, migration files, and bundled assets. This is static, read-only data. Your gadget reads from it through the assets import to access bundled files like word lists, icon sets, or configuration templates.

Each gadget gets its own writable data directory at <app-data>/gadget-home/<gadget-id>/. This is where runtime state lives: the SQLite database (sql/storage.sqlite3), the compiled WASM cache, and reserved slots for future storage types like blob caches or temporary files. The host creates this directory automatically when the gadget is first enabled. Your gadget interacts with it primarily through the sql-storage import, but the path is also available for use in filesystem or command permission rules.

Two substitution variables give your gadget access to both locations at runtime:

Variable Resolves to
${gadget-archive} The gadget’s read-only code root (archive or directory).
${gadget-data} The gadget’s writable data directory.

This separation keeps code and state cleanly isolated. Uninstalling a gadget removes both locations, so no orphaned data is left behind. Reinstalling starts fresh with an empty data directory.