npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@intellia/app-sdk

v0.8.0

Published

Build typed, gated embedded apps for the Intellia platform.

Readme

@intellia/app-sdk

Build typed, gated embedded apps for the Intellia platform.

A Hutly app is a single-file HTML bundle that runs inside a Hutly canvas. It never talks to the backend directly — it calls workflows and reads/writes tables through the window.hutly client that this package installs, and the build toolchain gates it: a broken or undeclared reference fails at build time, so a broken app can never publish.

Install

yarn add @intellia/app-sdk

The CLI commands below (intellia app ...) come from @intellia/cli.

Quickstart

intellia app init my-app
cd my-app && yarn install

This scaffolds a project with a hutly.manifest.ts, an index.html entry, and a Vite build already wired for a single-file bundle.

1. Declare what your app needs

Every workflow slug and table your app touches must be declared in hutly.manifest.ts:

import { defineManifest } from "@intellia/app-sdk";

export default defineManifest({
  workflows: ["create-lead"],
  tables: [{ table: "leads", access: "write" }],
});

defineManifest is an identity function that gives you type checking against the AppManifest shape — nothing more.

An app that needs to send a user to a URL outside the platform lists it under externalUrls, an optional array of https://-only patterns:

export default defineManifest({
  workflows: ["create-lead"],
  tables: [{ table: "leads", access: "write" }],
  externalUrls: ["https://*.hutly.com/*"],
});

See "Route and theme with the host" below for what navigate does with a URL that matches.

2. Call the backend through window.hutly

import "@intellia/app-sdk"; // installs window.hutly

if (!window.hutly) throw new Error("not running inside a Hutly host");
const ctx = await window.hutly.ready;
if (ctx.signedIn) {
  const result = await window.hutly.call("create-lead", { name: "Ada" });
  // orderBy is comma-joined "field:asc|desc" segments — never "-field".
  const leads = await window.hutly.tables.query("leads", {
    limit: 20,
    orderBy: "id:desc",
  });
}

window.hutly.call(workflowSlug, input) and window.hutly.tables.* (query, insert, update, delete) are the only way an app reaches the backend. There is no raw fetch, no API key, and no direct database access available to app code — the host validates every call against the app's approved manifest before it runs.

3. Route and theme with the host

An app can see the host's real sub-path/query and live theme, and navigate without a full reload:

import "@intellia/app-sdk";

if (!window.hutly) throw new Error("not running inside a Hutly host");

window.hutly.route.get(); // { path: "/inbox/42", search: "?foo=bar" }
window.hutly.route.subscribe(() => console.log(window.hutly?.route.get()));

window.hutly.theme.get(); // "light" | "dark", follows the host's live theme
window.hutly.theme.subscribe(() => console.log(window.hutly?.theme.get()));
window.hutly.theme.set("dark"); // pin a fixed theme; set("auto") releases it

await window.hutly.navigate("/inbox/43", { replace: true }); // relative to
// this app's own mounted sub-path, not the whole site

// On an app site, an absolute URL on the site's own origin routes the site to
// another app mounted there — see "Linking to another app on the same site":
await window.hutly.navigate("https://acme.apps.hutly.com/industry-awards");

// Any other absolute https:// URL is accepted if it matches a pattern in
// externalUrls. It opens in a new tab unless you say otherwise:
await window.hutly.navigate("https://docs.hutly.com/apps");
await window.hutly.navigate("https://docs.hutly.com/apps", { target: "_self" }); // same tab
await window.hutly.navigate("https://docs.hutly.com/apps", { replace: true }); // same tab, no history entry

| opts | result | |---|---| | (none) or { target: "_blank" } | new tab | | { target: "_self" } | same tab (location.assign) | | { replace: true } | same tab (location.replace), no history entry | | { target: "_blank", replace: true } | rejected — a new tab that replaces itself is a contradiction |

A pattern's host may lead with *. to match one or more subdomains — it never matches the bare apex, so list the apex as its own entry if you need it — over a registrable domain; a public suffix like co.uk or github.io is refused, since it would grant every site hosted under it. Path segments are literal or a bare *: a trailing /* grants everything under that path at any depth, including none (/docs/* matches /docs); a * earlier than the last segment matches exactly one non-empty segment (/customer/*/cart, never /customer/8/1/cart). A trailing slash is insignificant, so /customer/*/ grants the same depth as /customer/* — not "one segment under /customer".

A URL the manifest doesn't allow rejects with navigate_denied. One the browser's popup blocker refused rejects with navigate_blocked — the host shows the visitor a recovery dialog rather than failing silently.

Linking to another app on the same site

An app site mounts several published apps on one host, each at its own path. From one of them, a plain anchor to another is all you need — the SDK claims the click and asks the site to route it, so the visitor lands on the other app with no full page load and no SDK call in your code:

<a href="/industry-awards">Industry awards</a>

Two rules decide where such an href points:

  • Root-relative (/industry-awards) resolves against the site root.
  • Relative (awards, ./awards) resolves against your app's mount — /events/awards for an app mounted at /events. That is the mount, not your app's current in-app route: <a href="detail"> from /inbox/42 goes to <mount>/detail, never <mount>/inbox/detail. Your document is served from a blob: URL whose base has no path to resolve against, so the mount is what the SDK substitutes.

The SDK claims a click only when it is a plain left click with no modifier key, on an anchor whose target is absent, empty, _self, _top or _parent (matched case-insensitively, so _SELF counts), and whose href lands on the site's own origin. Everything else stays the browser's: target="_blank" and named targets, in-page #hash links, mailto:, another origin. A handler that already called preventDefault() wins, so a router's <Link> keeps its own behaviour.

window.hutly.navigate reaches the same destination from code, as an absolute URL on the site's origin:

await window.hutly.navigate("https://acme.apps.hutly.com/industry-awards");

A root-relative <a href> is the portable form. A hard-coded https://<slug>.apps.hutly.com/… is a different origin to a visitor who reached the site on its custom domain, so it takes the external path: denied unless externalUrls allowlists it, and navigate_unsupported on a public page, which opens no external URLs. Use the root-relative form.

An app can only reach another app on the same site. The site resolves the target path against its registry of mounted apps and refuses anything else, so /sso and every other route the shell serves itself is unreachable from app code. A refused destination rejects with host_error and the message navigate_failed; an intercepted anchor click has no promise to reject, so the SDK logs [hutly-sdk] link navigation failed and the click does nothing.

All of this exists only on an app site. The in-workspace App viewer has no sibling apps to reach: it leaves anchors to the browser, and an absolute URL on the workspace's own origin is treated like any other external destination — denied, and logged, unless externalUrls grants it.

You cannot exercise a sibling link in intellia app dev. The local harness is not a site either, so the SDK leaves the anchor to the browser and the harness's static server has no such file — it answers 404, and the click replaces your app in the frame with a plain "not found". In the workspace App viewer the same link does nothing at all, because the app document there has no base path for the browser to resolve it against. Neither outcome is a fault in your markup, and neither is worth debugging: a sibling link only resolves on a real app site, so that is where to check it.

An app mounted at / is matched on the exact path / only. Any deeper path resolves to whichever other app is mounted at that prefix, or to nothing at all, so a root-mounted app cannot route the host to its own sub-paths. Mount an app that needs its own routes at a real path (/events) rather than at the site root.

Building with React, @intellia/app-sdk/react wraps this in the usual hooks and a react-router-dom integration:

import { useHutlyRoute, useHutlyTheme, HutlyRouter } from "@intellia/app-sdk/react";
import { Routes, Route } from "react-router-dom";

// useHutlyRoute() / useHutlyTheme() are useSyncExternalStore wrappers around
// window.hutly.route / .theme — re-render on every live change.

createRoot(document.getElementById("app")!).render(
  <HutlyRouter>
    <Routes>
      <Route path="/*" element={<App />} />
    </Routes>
  </HutlyRouter>,
);

HutlyRouter bridges window.hutly.route/.navigate() into an ordinary react-router-dom <Router>, so useNavigate(), <Link>, and nested <Routes> all work as normal — a click on a <Link to="/inbox/43"> calls window.hutly.navigate under the hood, so it always stays inside your own app. Use a plain <a href> to reach another app on the site. intellia app init scaffolds an app this way by default; pair it with @intellia/app-ui for on-brand primitives.

Save a file or print a document

An app runs in a sandboxed frame that cannot download or open the print dialog itself, so the host does both for it. Declare what the app needs:

export default defineManifest({
  workflows: [],
  tables: [{ table: "orders", access: "read" }],
  downloads: true, // hutly.files.download()
  print: true, // hutly.print()
});

Both show in the approval dialog, so a manifest that adds one needs re-approving. ctx.downloads / ctx.print on await window.hutly.ready say whether the call will work here; they are false without the grant and in the mobile app, which can do neither (calls there reject unsupported_platform).

const ctx = await window.hutly.ready;

// The file is saved on the user's device; nothing is uploaded to the workspace.
const { filename } = await window.hutly.files.download({
  filename: "orders-2026-10.csv",
  mimeType: "text/csv",
  data: "\uFEFFid,total\n1,42.00\n", // string, Blob, ArrayBuffer or Uint8Array
});

// Opens the print dialog, where the user can also choose Save as PDF.
await window.hutly.print({
  title: "Invoice INV-0042", // the suggested PDF file name
  html: `<style>body { font-family: sans-serif }</style><h1>Invoice INV-0042</h1>`,
});

files.download saves text/csv, text/plain, application/json, application/pdf and .xlsx, up to 20 MB; any other type rejects with disallowed_file_type (HTML and SVG are refused because they run when opened from disk) and a larger file with file_too_large. The filename loses path separators and characters operating systems refuse, and gains the type's extension if it lacks it (report.exe as CSV saves as report.exe.csv); the result is the name it was saved under. Excel reads a CSV as UTF-8 only with a leading BOM (\uFEFF). A user-input cell that starts with =, +, - or @ runs as a formula even when quoted, so prefix it with an apostrophe before the usual CSV quoting: =1+1 becomes '=1+1, not "=1+1".

Both need the user's gesture, so call them from a click handler. Within about 5 seconds of the click the file saves (or the dialog opens) at once; a request later than that, such as an export that spent longer fetching rows, or a second one on the same click, makes the host show " wants to save orders.csv" with Save and Cancel, and the action runs only when the user presses it. Cancel, or two minutes without an answer, rejects output_cancelled. One request waits at a time (output_busy) and at most 5 run a minute (output_rate_limited), so an app cannot loop dialogs or spray files. Neither call has a deadline while the host waits.

print takes up to 5 MB of HTML and prints a copy with scripts, event handlers, forms, frames, <link> and <meta> tags, and every remote URL removed, under a policy that blocks network loads. Put CSS in a <style> element (use @media print and @page as normal) and embed images as data:image/... URLs. The promise resolves once the dialog has opened. Without the grant the calls reject downloads_not_in_manifest / print_not_in_manifest.

4. Build

intellia app build

build runs, in order: manifest-aware codegen, tsc --noEmit, eslint ., a manifest-consistency check, then the Vite single-file bundle, then publishes the resulting dist/index.html as an artifact. Any failure aborts the whole command — nothing partial gets published.

Use intellia app validate to run everything except the bundle/publish step (useful in CI or as a pre-commit check).

The build gate

The toolchain enforces two things no amount of hand-authored HTML can bypass in intellia app build/validate:

  • Undeclared references fail. If your source calls hutly.call("some-slug") or hutly.tables.query("some-table") for a slug or table not listed in hutly.manifest.ts, validate/build fails with a manifest-consistency error.
  • Shape mismatches fail. intellia app codegen generates hutly.gen.ts typings from your manifest's real workflow/table schemas; a wrong input shape is a tsc compile error, not a runtime surprise.

The generated ESLint ruleset (@intellia/app-sdk/eslint, already wired into the scaffolded eslint.config.js) 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 (approved manifest ∩ viewer capability) lives in the Hutly host, not in this toolchain.

What's in this package

  • Runtime client (window.hutly) — bundled by intellia app build into your app; also available as @intellia/app-sdk for tsc/type checking.
  • defineManifest, HutlyError, PROTOCOL_VERSION — exported from the package root.
  • @intellia/app-sdk/react — useHutlyRoute, useHutlyTheme, HutlyRouter.
  • @intellia/app-sdk/eslint — the shared ESLint ruleset described above.
  • @intellia/app-sdk/template — the project scaffold used by intellia app init, built on @intellia/app-sdk/react and @intellia/app-ui.