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”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.
Uninstalling
Section titled “Uninstalling”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.
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.