HUB.KI DocsCustom apps
HUB.KI Docs
Custom apps

Quick start

Build a minimal custom app, run it locally, and package it for HUB.KI.

View as Markdown

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 notes

The @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-singlefile

Delete 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 dev

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

The 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.html

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