# Quick start (https://docs.hub.ki/custom-apps/quick-start)



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](https://vite.dev) and
TypeScript; any toolchain works as long as the result is a
[single HTML file](https://docs.hub.ki/custom-apps#constraints).

Working with a coding agent? [Give it the custom-apps skill](https://docs.hub.ki/custom-apps/agent-skill)
and it follows these steps for you.

## 1. Create the project
```sh
npm create vite@latest notes -- --template vanilla-ts
cd notes
```

The [`@hub-ki/app-sdk`](https://github.com/orgs/hub-ki/packages/npm/package/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:

```ini
@hub-ki:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
```

```sh
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:

```html
<!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:

```ts
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:

```ts
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](https://docs.hub.ki/custom-apps/bridge).

## 3. Run it locally
```sh
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](https://docs.hub.ki/custom-apps/sdk#developing-without-hubki)
for everything the mock can simulate.

## 4. Build and package
```sh
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`:

```sh
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:

```json
{
  "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](https://docs.hub.ki/custom-apps/manifest) 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](https://docs.hub.ki/custom-apps/publishing)
for the upload and [Administration](https://docs.hub.ki/custom-apps/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.
