Manifest
The manifest.json reference — fields, scopes, REST API scopes, and versioning.
Every app folder contains a manifest.json. The platform validates it when it
syncs a source; a manifest that fails validation is skipped with an error.
{
"appId": "ai.justadd.stickyboard",
"slug": "stickyboard",
"name": { "en": "Stickyboard", "de": "Stickyboard" },
"version": "2.3.0",
"sdkVersion": "1.0.0",
"artifactPath": "app.html",
"iconPath": "icon.svg",
"events": ["board-updated"],
"scopes": ["workspace:own"],
"apiScopes": ["meetings:read"],
"connectOrigins": ["https://api.example.com"]
}Fields
| Field | Type | Required | Description |
|---|---|---|---|
appId | string | yes | Globally unique identifier. Reverse-DNS is recommended (com.example.app). |
slug | string | yes | Short, URL-safe name used in the app's route. |
name | Record<locale, string> | yes | Localized display names. Include at least en; it is the fallback. |
version | string | yes | App version, numeric and dotted (1.4.0, optionally 1.4.0-beta.1). Bump it to ship an update. |
sdkVersion | string | yes | Bridge contract you target, as exact major.minor.patch. The host supports major 1. |
artifactPath | string | yes | Path to the single HTML file, relative to the folder. |
icon | string | * | A Lucide icon name, e.g. sticky-note. |
iconPath | string | * | Path to an icon image instead (svg, png, jpg, webp, gif; ≤ 256 KB). |
events | string[] | no | Event names the app may publish or receive. |
description | { [locale]: string } | no | Short description shown on the marketplace card, keyed by locale like name. |
scopes | string[] | no | Capabilities the app requests (see below). |
apiScopes | string[] | no | HUB.KI REST API scopes the app requests (see REST API scopes). |
connectOrigins | string[] | no | Backend origins the app may call from the browser (see Network access). |
* Provide either icon or iconPath.
Scopes
Scopes are the only capabilities an app has beyond rendering itself. Request the minimum you need.
| Scope | Grants | Approval |
|---|---|---|
workspace:own | The app's own private, per-user workspace (documents and files). | None — always available. |
store:own | A per-user, per-version key/value store for the app. | None — always available. |
documents:read | Read the user's shared organization workspace documents. | Org admin must approve the scope. |
user:read | Read the signed-in user's name and email. | Org admin must approve the scope. |
api:access | Hand the app's own backend short-lived tokens for the user (see Backend access). | Org admin must approve the scope. |
responses:run | Run the HUB.KI AI (Responses API) at the organization's expense via an automatically issued API key. | Org admin must approve the scope. |
capability:wasm | Run WebAssembly in the app sandbox (wasm-unsafe-eval). | Org admin must approve the scope. |
workspace:own and store:own are granted automatically. Any other scope must
be approved by an organization admin from Manage availability before the app
can use it — an un-approved call is rejected.
Web Workers are available to every app (worker-src blob:), so heavy work can
leave the main thread without asking for anything. WebAssembly is different: it
widens what may execute in the sandbox, so it is opt-in through
capability:wasm and, like any other approval, the org admin sees it before
turning it on. Libraries such as pdf.js that can run either way will fall back to
their non-WASM path when it is not granted.
REST API scopes
apiScopes lists the parts of the HUB.KI REST API (/v1/*) the app wants to
call as the user who has it open — from the app through
createAppApiClient / requestApi, or from its backend
with the REST API token. Every
REST API scope except responses:write is allowed; apps run AI through the
responses:run scope and hub.responses instead.
"apiScopes": ["meetings:read", "documents:write"]| Scope | Allows |
|---|---|
documents:read | List, get and search workspace documents, and read their children and ancestors. |
documents:write | Create, update, delete, move and share documents. |
meetings:read | Read meetings, their summaries and transcripts, and meeting series. |
meetings:write | Rename and delete meetings, and create one from an upload. |
files:read | Read a file and get its download URL. |
files:write | Upload and delete files. |
agents:read | List agents. |
skills:read | List skills. |
skills:write | Create personal skills as the user and update skills that user created. |
connectors:read | Read connectors, connections, their tools and tool-call policies. |
connectors:write | Change a connector's tool-call policy. |
connectors:invoke | Run connector tools. |
connectors:approve | Run connector tools that need approval, asserting the approval on the user's behalf. |
usage:read | Read API usage (/v1/usage). |
statistics:read | Read usage statistics (/v1/statistics). |
traces:read | Read traces (/v1/traces). |
tracing:read | Read the organization's trace ledger (/v1/tracing); only for users who may view tracing. |
tracing_content:read | Read the conversations of chats shared for debugging (/v1/tracing/traces/{traceId}/content, with tracing:read); only for users who may view tracing. |
chats:read | Read chats (/v1/chats). |
helpdesk:read | Read helpdesk tickets and their messages, and list helpdesk agents. |
helpdesk:write | Create tickets and replies, and update tickets. |
With any approved REST API scope the app can also call GET /v1/models and
GET /v1/jobs.
Like scopes, declaring a REST API scope is a request. An organization admin
approves each one in Manage availability; scopes that change data or act on
the user's behalf — every *:write, connectors:invoke, connectors:approve,
tracing:read and tracing_content:read — are marked Sensitive there. Requests act as the user
who has the app open, so they never reach more than that user may see. You do
not need api:access to call the REST API from the app; you need it only to
hand the REST API token to your backend.
An entry that is not in the table above — including responses:write — skips
the app on sync with the problem api_scopes_unknown.
Validation
A sync validates every manifest and its files. An app that fails any rule is skipped as a whole — and if it was published before, it goes offline (see Syncing). The admin interface only reports the number of skipped apps, so check these before uploading:
| Rule | Typical mistake |
|---|---|
| The file is valid JSON and every required field is present. | Trailing comma; missing name or artifactPath. |
appId: letters, digits, ., -, _; segments never empty; at most 255 characters. | Spaces, a leading or trailing dot. |
slug: lowercase letters and digits separated by single hyphens; at most 128 characters. | Uppercase letters, underscores, spaces. |
sdkVersion: exactly major.minor.patch with a supported major (1). | "1", "1.0", "^1.0.0", "2.0.0". |
icon or iconPath is set. An icon file is svg, png, jpg, webp or gif, ≤ 256 KB. | Neither given; icon too large. |
Every entry in scopes is a known scope (table above); at most 20. | A typo such as storage:own. |
Every entry in apiScopes is a REST API scope; at most 22. | responses:write; a typo such as meeting:read. |
Every entry in connectOrigins is an exact https:// or wss:// origin (see below); at most 8 are used. | A path, a wildcard, plain http:// or ws://. |
artifactPath points to an existing file of at most 10 MB. | Manifest says app.html, folder holds index.html. |
The artifact starts like an HTML document (<!doctype html or <html near the top). | Uploading a JavaScript bundle instead of the page. |
appId + version appears in only one folder of the source. | Copying a folder without bumping version. |
appId is not already published by another source. | Reusing an example appId. |
Further limits: name values up to 200 characters, description values up to
2,000, version up to 32, and up to 100 events of up to 100 characters each.
Network access
Apps run in a sandboxed iframe whose Content-Security-Policy blocks all
network requests by default — fetch, XMLHttpRequest, WebSocket,
EventSource, and sendBeacon to any origin, including your own backend. To let
your app talk to a server, list its exact origins in connectOrigins:
"connectOrigins": ["https://api.example.com", "wss://realtime.example.com"]Rules:
- Each entry must be an exact origin — scheme + host + optional port, no path,
no wildcards. Use
https://for HTTP requests andwss://for WebSockets;http://andws://are allowed only forlocalhost/127.0.0.1during development. Up to 8 origins. - The scheme is part of the origin.
https://api.example.comdoes not allowwss://api.example.com, so a host you call with bothfetchandWebSocketneeds two entries. Count them against the limit of 8 when you plan your manifest. - Declaring an origin is a request, not a grant. An organization admin
approves each origin in Manage availability; only approved origins are
added to the app's
connect-src. Unapproved origins stay blocked. - Requests and WebSocket handshakes from the app carry
Origin: null, because the sandbox has no origin of its own. Your server must accept that origin. connect-srconly covers connections the page opens itself. WebRTC media traffic is not governed by it, but WebRTC SDKs usually open awss://signaling connection, and some also fetch configuration overhttps://— declare both.
See Calling HUB.KI from your backend for the end-to-end flow and the CORS requirement.
Versioning
versionidentifies your release. Publish a newversionto roll out changes; the platform re-reads the artifact on its next sync.sdkVersiondeclares the bridge contract. If its major version is not supported, the host refuses to run the app and shows an upgrade notice.