Building an Applet
Applet Packages
On this page
Introduction
An applet is built output. Whatever your bundler emits, plus a manifest and a bill of materials, goes into a single zip, and that zip is the only thing a Rack ever receives from you.
This page covers what goes in the zip, the rules a Rack applies when you upload one, and how to build a package that passes them the first time.
The Zip
An applet ships as a single zip, named for the applet and the version it contains:
{slug}-{version}.zip
├── manifest.json required, at the zip root
├── sbom.json required — CycloneDX, or package-lock.json
├── index.html the entry (configurable via manifest.entry)
├── migrations/… optional — data migrations, one ES module per file
└── assets/… everything the entry references, by RELATIVE pathmigrations/ is the one other directory the Rack reads. It holds the data migrations that reshape existing records when the version is promoted. To the installer, its files are ordinary .js or .mjs files, named YYYY_MM_DD_HHMMSS_name.js and sitting directly inside it. Any other name there refuses the package.
Apart from those, nothing is special. The Rack extracts the archive, keeps the file tree intact, and serves it as uploaded. There is no build step on the server, no server-side templating, and no framework to satisfy. Whatever your bundler emits is what runs.
Installer Rules
The installer's rules are part of ziprack/1, so every Rack applies them identically. One bad file refuses the whole upload rather than being skipped:
- Static extensions only.
html js mjs css map json wasm txt vtt svg png jpg jpeg gif webp ico woff woff2 ttf mp3 mp4 wav webm ogg pdf. A server-executable file anywhere in your build output rejects the package. - No
.., no absolute paths, no symlinks, no NUL bytes. Dotfiles and__MACOSX/are skipped silently, so a zip built on macOS is fine. - A size ceiling. 50 MB by default; each Rack sets its own.
manifest.entrymust exist in the zip and end in.html.- The slug must match the applet you are uploading to, and it is immutable once created.
- A
versionstring uploads once per applet. Bump it every time, including during testing.
Installation is all or nothing. Each declared capability also checks its own parts of the package at upload, such as db@1 seed files and migration names, and a failure anywhere refuses the upload and leaves nothing behind. Installing never touches the applet's existing records; those are only changed at promotion.
The Starter
The zipRACK starter is a minimal, working ziprack/1 applet: plain Vite and TypeScript, @ziprack/sdk, one question, one statement, one completion. To get started, install its dependencies and run it:
npm install
npm run dev # mock mode: the whole experience runs standalone
npm run package # dist-zip/{slug}-{version}.zip, ready to uploadEdit public/manifest.json first: the slug, name, version, and the capabilities you intend to call. Everything else follows from there.
Relative URLs
A Rack serves your package from a version-scoped path, not the site root. A root-relative URL like /assets/app.js therefore 404s in production while working perfectly on your dev server. It is the single most common packaging mistake.
With Vite, one line prevents it:
export default defineConfig({
base: './', // required
build: { outDir: 'dist', emptyOutDir: true },
});The same applies to anything you write by hand: ./assets/logo.svg, never /assets/logo.svg.
The SBOM
Every package carries a sbom.json (CycloneDX) or a package-lock.json at its root. An applet is built output, so the lockfile is the only record of the dependencies inside it.
On upload, the Rack checks the packages it lists against a vulnerability database. Findings attach to the version for review rather than blocking the upload.
Building and Packaging
In the starter, npm run package does the whole job:
// scripts/package.mjs — abbreviated
const manifest = JSON.parse(readFileSync('dist/manifest.json', 'utf8'));
execSync('npm sbom --sbom-format cyclonedx --omit dev', { cwd: lockDir }) // → dist/sbom.json
cpSync('migrations', 'dist/migrations', { recursive: true }); // when present
execSync(`zip -r -X "${zipPath}" . -x ".*" -x "__MACOSX*"`, { cwd: 'dist' });If you write your own, three details are worth keeping. -X drops extra file attributes. Excluding dotfiles keeps .DS_Store out of an archive the installer would otherwise skip anyway. And a top-level migrations/ is copied in as written, never bundled, because the Rack imports each file on its own.
Once your zip is built, upload it to the Rack. Uploads land unpublished: promotion is always a separate, deliberate action. See Review & Release.