Documentation ziprack/1SDK 0.3.0

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 path

migrations/ 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.entry must 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 version string 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:

Shell
npm install
npm run dev        # mock mode: the whole experience runs standalone
npm run package    # dist-zip/{slug}-{version}.zip, ready to upload

Edit 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:

TypeScript
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:

JavaScript
// 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.