Apps
A Hutly app is a small, typed, single-file HTML app that a user pins as
an App inside their Hutly workspace. It runs sandboxed inside the
platform and only ever reaches the backend through a window.hutly client
that the host injects at runtime — there is no raw fetch, no API key, and
no direct database access available to app code.
Apps are built and published with the Hutly CLI (hutly) via
@intellia/app-sdk. See the CLI Reference for how the CLI
itself is installed and authenticated.
The loop
hutly app init my-app
cd my-app && yarn installhutly app init <dir> scaffolds a project: hutly.manifest.ts, an
index.html entry, a src/main.ts that installs window.hutly, and a Vite
config already wired to produce a single-file bundle.
1. Declare scope in hutly.manifest.ts
Every workflow slug and table an app touches must be declared up front:
import { defineManifest } from "@intellia/app-sdk";
export default defineManifest({
workflows: ["create-lead"],
tables: [{ table: "leads", access: "write" }],
});defineManifest is an identity function — it only exists to type-check the
object against the AppManifest shape. This manifest is the app’s entire
declared scope: nothing outside it will ever be callable.
2. Write the app against window.hutly
import "@intellia/app-sdk"; // installs window.hutly
const ctx = await window.hutly.ready;
if (ctx.signedIn) {
const lead = await window.hutly.call("create-lead", { name: "Ada" });
const leads = await window.hutly.tables.query("leads", { limit: 20 });
}window.hutly.call(workflowSlug, input) and window.hutly.tables.query /
insert / update / delete are the only way an app reaches the
backend. Run hutly app codegen to generate hutly.gen.ts typings from the
manifest’s real workflow input schemas and table columns, so a wrong input
shape becomes a tsc error rather than a runtime surprise.
3. Build
hutly app buildbuild runs, in order: manifest-aware codegen → tsc --noEmit → eslint .
→ a manifest-consistency check → the Vite single-file bundle → publish the
resulting dist/index.html as an artifact. Any step failing aborts the whole
command, so a broken app can never publish. hutly app validate runs the
same checks without the bundle/publish step, useful in CI or as a pre-commit
check.
The window.hutly surface
| Member | Shape | Notes |
|---|---|---|
version |
number |
Protocol version the running host speaks. |
ready |
Promise<HutlyContext> |
Resolves once the host has handshaked; HutlyContext is { signedIn, organizationId?, user?, workflows, tables }. |
isSignedIn() |
() => boolean |
Synchronous read of the current context. |
getContext() |
() => HutlyContext |
Synchronous read of the full context. |
call(slug, input?) |
Promise<T> |
Invokes a workflow by slug. |
tables.query/insert/update/delete |
Promise<T> |
CRUD against a declared table. |
call and every tables.* method reject with a typed HutlyError rather
than silently doing nothing. The error codes an app should be prepared to
handle: not_signed_in, app_not_approved, workflow_not_in_manifest,
table_not_in_manifest, protocol_mismatch, host_error, timeout.
The build gate
The toolchain enforces two things no amount of hand-authored HTML can bypass:
- Undeclared references fail. Calling
hutly.call("some-slug")orhutly.tables.query("some-table")for a slug or table not listed inhutly.manifest.tsfailsvalidate/buildwith a manifest-consistency error. - Shape mismatches fail. The generated
hutly.gen.tstypings come from the manifest’s real workflow/table schemas, so a wrong input shape is atsccompile error, not a runtime surprise.
The scaffolded ESLint config additionally bans raw fetch,
XMLHttpRequest, WebSocket, dynamic import(), and document.write in
app source — the only sanctioned backend access is window.hutly.
This is a correctness/DX gate, not the security boundary — the runtime enforcement that actually contains an app (an approved manifest intersected with the viewer’s capability) lives in the Hutly host, not in the build toolchain.
Approval
Building and publishing an app is not enough to make it live. An app only
runs once its artifact is pinned as an App, and pinning an artifact whose
declared manifest isn’t approved yet stops to show an admin a plain-language
scope dialog listing the workflow slugs it can run and the tables it can
read or write. An admin has to explicitly approve that scope and pin — until
they do, every window.hutly.call/tables.* invocation from that artifact
fails with app_not_approved. This is enforced on every request by the host,
not just in the pin-time dialog.
Approval is scoped to a specific artifact version. Publishing a new version
of an app is a new version with its own approval state, so expect to
re-approve after a rebuild/republish if app_not_approved shows up again.