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.

To install a gadget, place its .torchsnap archive in the user gadgets directory. The host validates the archive on install: it checks that the zip is valid, that manifest.toml is present and parseable, and that all referenced paths pass validation. If a gadget with the same ID is already loaded, the install is rejected.

Only user-installed gadgets can be uninstalled. Uninstalling removes the archive file, the gadget’s entire state directory (database, compile cache), and all settings keys in the gadget’s namespace. System 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.