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.
Archive contents
Section titled “Archive contents”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.
Path validation
Section titled “Path validation”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.
Creating an archive
Section titled “Creating an archive”A .torchsnap archive is a zip file with manifest.toml at the root. To
create one manually:
- Build the WASM binary (targeting
wasm32-wasip2). - Build the frontend bundles if your gadget has a UI (typically via Vite).
- Collect the manifest, WASM binary, frontend output, migration files, and any bundled assets into a directory.
- 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.
What goes into the archive
Section titled “What goes into the archive”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
.wasmbinary (output ofcargo 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.
Development
Section titled “Development”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:
- Edit your gadget’s Rust source or frontend code.
- Rebuild with
just build-gadget <gadget-id>. - 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.
Installing and uninstalling
Section titled “Installing and uninstalling”Installing
Section titled “Installing”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] migrationsthan the installed one, since the new version could not open the installed version’s database.
Publishing an update
Section titled “Publishing an update”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.
Testing the install flow
Section titled “Testing the install flow”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.
Uninstalling
Section titled “Uninstalling”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.
Storage layout
Section titled “Storage layout”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.
Gadget archive (read-only)
Section titled “Gadget archive (read-only)”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.
Gadget data (writable)
Section titled “Gadget data (writable)”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.
Substitution variables
Section titled “Substitution variables”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.