Quick start
Build a minimal custom app, run it locally, and package it for HUB.KI.
This walkthrough builds a small notes app that follows the host theme and saves its text in the app's key/value store. It uses Vite and TypeScript; any toolchain works as long as the result is a single HTML file.
Working with a coding agent? Give it the custom-apps skill and it follows these steps for you.
1. Create the project
npm create vite@latest notes -- --template vanilla-ts
cd notesThe @hub-ki/app-sdk
package is published to the GitHub npm registry, which requires a token even for
public packages. Add an .npmrc to the project that maps the @hub-ki scope,
with GITHUB_TOKEN set to a personal access token (classic) that has the
read:packages scope:
@hub-ki:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}npm install @hub-ki/app-sdk
npm install -D vite-plugin-singlefileDelete the template's sample files (src/counter.ts, src/style.css,
src/assets/, public/); the three files below replace them.
2. Write the app
index.html — everything the app needs is inline, including the styles:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Notes</title>
<style>
:root {
font-family: system-ui, sans-serif;
color-scheme: light dark;
}
[data-theme="light"] {
background: #fff;
color: #111;
}
[data-theme="dark"] {
background: #111;
color: #eee;
}
body {
margin: 0;
padding: 16px;
display: grid;
gap: 8px;
}
textarea {
min-height: 160px;
font: inherit;
}
</style>
</head>
<body>
<textarea id="note" placeholder="Write a note…"></textarea>
<button id="save" type="button">Save</button>
<script type="module" src="/src/main.ts"></script>
</body>
</html>src/main.ts — connect, follow the theme, load and save the note:
import { connect } from "@hub-ki/app-sdk";
// Outside HUB.KI nothing answers the handshake, so `vite dev` uses the SDK's in-memory host.
const hub = await connect({
mock: import.meta.env.DEV
? { scopes: ["store:own"], hostContext: { theme: "light" } }
: undefined,
});
// Apply the host theme, and keep it in sync.
document.documentElement.dataset.theme = hub.context.theme;
hub.onContextChange((context) => {
document.documentElement.dataset.theme = context.theme;
});
// Load the saved note, and persist it on demand.
const note = document.querySelector<HTMLTextAreaElement>("#note")!;
const save = document.querySelector<HTMLButtonElement>("#save")!;
note.value = (await hub.store.get("note")) ?? "";
save.addEventListener("click", () => {
void hub.store.set("note", note.value);
});vite.config.ts — inline all JavaScript and CSS into one HTML file:
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
export default defineConfig({ plugins: [viteSingleFile()] });Prefer no dependencies? The SDK only wraps postMessage — you can implement the
protocol directly. See The bridge.
3. Run it locally
npm run devOutside the platform there is no host to answer the bridge, and connect()
would time out. The mock option above swaps in an in-memory host while
developing, so the store, the theme and every other call answer locally. It is
only enabled in dev mode; the production build talks to the real host. See
Developing without HUB.KI
for everything the mock can simulate.
4. Build and package
npm run buildThe build writes a single file, dist/index.html. An app is a folder holding
that artifact next to a manifest.json:
mkdir -p publish/notes
cp dist/index.html publish/notes/app.htmlpublish/notes/manifest.json — the store:own scope lets the app use its
private key/value store:
{
"appId": "com.example.notes",
"slug": "notes",
"name": { "en": "Notes" },
"version": "1.0.0",
"sdkVersion": "1.0.0",
"artifactPath": "app.html",
"icon": "sticky-note",
"scopes": ["store:own"]
}artifactPath must name the file you copied (app.html here — or keep
index.html and point the manifest at it). sdkVersion is the bridge contract
you target, not your app's version; any 1.x.y runs on the current host. See the
Manifest reference for every field and the
validation rules.
5. Publish it
Upload the folder to a source the platform reads, then have an administrator sync it and make it available — see Publishing for the upload and Administration for the admin side. A freshly synced app is listed in the Marketplace as Not available until an organization admin chooses who may use it.