Skip to content

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 install

hutly 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 build

build runs, in order: manifest-aware codegen → tsc --noEmiteslint . → 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") or hutly.tables.query("some-table") for a slug or table not listed in hutly.manifest.ts fails validate/build with a manifest-consistency error.
  • Shape mismatches fail. The generated hutly.gen.ts typings come from the manifest’s real workflow/table schemas, so a wrong input shape is a tsc compile 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.