Mini apps
App folder
The fixed folder structure a mini app is made of, the rules a folder must follow to deploy, and how to deploy one from VS Code, a script or an AI coding tool.
App folder
Every mini app is one folder in a fixed structure. Whether the app was built by chatting, written by hand in VS Code, or written by an AI coding tool, it is the same folder β and only a folder in this structure can be deployed.
my-app/
tokenharbor.json the app's name, address, icon and colour required
index.html the page required
instructions.md what the app's AI model is told optional
style.css any number of .css and .js files, optional
js/app.js in any sub-folders, loaded from index.html
assets/ pictures, fonts and sounds optional
logo.png
Start a new one with the deploy script: node miniapp-deploy.mjs init my-app writes this folder with a working page in it.
tokenharbor.json
{
"slug": "menu-reader",
"name": "Menu Reader",
"tagline": "Point your camera at a menu",
"icon": "π",
"color": "#c2410c"
}
| Field | ||
|---|---|---|
slug | required | The app's address: tokenharbor.ai/a/menu-reader. 3β32 lowercase letters, digits and hyphens, not starting or ending with a hyphen. Deploying a folder whose slug is free creates the app; deploying one whose slug is yours updates it. |
name | required | 1β40 characters. |
tagline | optional | One line, up to 80 characters. |
icon | optional | One emoji. |
color | optional | Six-digit hex, like #1d4ed8. |
The files
index.htmlis the only page. Put every screen in it and switch between them with JavaScript..cssand.jsfiles can be anywhere in the folder.index.htmlloads each one with a relative path β<link rel="stylesheet" href="css/style.css">,<script src="js/app.js"></script>β in the order they should run.assets/holds pictures (png, jpg, webp, gif, svg, ico), fonts (woff2, woff) and sounds (mp3, wav, ogg). Refer to them with relative paths from HTML, CSS or JavaScript:<img src="assets/logo.png">,url(../assets/bg.webp)incss/style.css,"assets/ding.mp3"in a script.instructions.mdis what the app's AI model is told before every message, up to 4,000 characters.
Files that are not part of an app are skipped and listed back to you, not refused: anything starting with a dot (.git/, .vscode/, .DS_Store) and README, LICENSE and CHANGELOG.
When you deploy, every stylesheet and script is placed inside the page and every asset is embedded in it, because the app runs sealed off from the network and cannot load files on its own. You keep writing separate files; nothing about how you write them changes.
The rules
- Plain HTML, CSS and JavaScript. No npm packages, no build tools, no TypeScript, JSX, Vue or Sass. If your project has a build step, build it and deploy the output folder (for example
dist/), as long as that folder follows this structure. - No network. Nothing loaded from
http(s)://β no CDN scripts, web fonts or remote pictures β and nofetch. Download what you need into the folder. Talk to the AI withth.chat.send. - No modules. Scripts cannot
import,exportorrequire; load each with its own<script src>, in order. - One page. No other
.htmlfiles. - No data files.
.json,.csv,.txtand the like cannot be fetched; put the data in a.jsfile as a variable.
Limits
| Files | 100 |
| One text file | 500 KB |
| All text files | 1 MB |
| One asset | 1 MB |
| All assets | 2 MB |
| Path | 200 characters, 6 levels deep |
Deploy with the script
The script needs Node.js 18 or later and an API key from Dashboard β API keys.
curl -fsSL https://tokenharbor.ai/miniapp-deploy.mjs -o miniapp-deploy.mjs
node miniapp-deploy.mjs init my-app # a new folder with a working page
export TOKENHARBOR_API_KEY=thk_live_...
node miniapp-deploy.mjs my-app # deploy it
It prints the app's address when the deploy is accepted, and every problem with how to fix it when it is not. Add --json to get the server's answer as JSON. It exits 0 when deployed, 1 when the folder was refused or its slug is taken, and 2 for anything else (no key, no network).
You can also deploy from the browser: open the app in Dashboard β Mini apps and choose Deploy a folder.
A deploy changes your draft only. You can open it at the app's address straight away; nobody else sees it until it is reviewed.
When a deploy is refused
Nothing is changed. The answer lists every problem at once, each with the file, a code, what is wrong and what to do:
{
"ok": false,
"error": { "code": "invalid_package", "message": "The app does not follow the mini app structure: 2 problems to fix. Fix them and deploy again." },
"problems": [
{
"file": "index.html",
"code": "external_reference",
"message": "It loads the script https://cdn.example.com/chart.js from the internet, which a mini app cannot reach.",
"fix": "Download the script into the folder, load it with a relative <script src>, and make sure it has no import/export."
},
{
"file": "src/App.tsx",
"code": "file_not_allowed",
"message": ".tsx files need compiling, and a mini app runs plain files only.",
"fix": "Build your app to plain .js and .css files and deploy those. If you write JavaScript by hand, use .js with no import/export."
}
],
"warnings": [],
"skipped": [".git/HEAD", "README.md"],
"guide": "https://tokenharbor.ai/docs/mini-apps/package",
"rules": "A mini app is one folder: tokenharbor.json (required: β¦"
}
| Code | Means |
|---|---|
file_not_allowed | A file that is not part of the structure: node_modules, build configuration, .ts/.jsx/.vue, a second .html, server code, a data file, an asset outside assets/. |
missing_manifest Β· bad_manifest Β· manifest_field | tokenharbor.json is missing, is not a JSON object, or a field does not fit. |
slug_mismatch | The folder's slug is not the app you are deploying to. |
missing_entry | No index.html, or it is empty. |
external_reference | Something is loaded from the internet. |
missing_reference | A path points at a file that is not in the folder. |
module_import Β· css_import | import/export/require in a script, or @import in a stylesheet. |
file_too_big Β· package_too_big Β· too_many_files | Over a limit. |
bad_path Β· bad_file | A path outside the folder, or content that is neither text nor base64. |
Warnings do not stop a deploy: not_loaded (a .css or .js file index.html never loads), network_call (the code calls fetch or similar, which will fail inside the app) and unknown_field (a field in tokenharbor.json that is not used).
For AI coding tools
If you are an AI tool deploying a mini app for a user, this is the loop:
- Write the folder in the structure above. Use
th.chat.sendfor anything AI; read the SDK reference. - Run
node miniapp-deploy.mjs <folder> --json(orPOST /api/miniapps/deploy, below). - If
okisfalse, apply each problem'sfixto the namedfile, and deploy again. Therulesfield states every rule in one paragraph. - Repeat until
okistrue. Give the user theurl.
A refused deploy changes nothing, so retrying is always safe. Deploys are limited to 30 a minute.
The deploy API
POST https://tokenharbor.ai/api/miniapps/deploy
Authorization: Bearer $TOKENHARBOR_API_KEY
Content-Type: application/json
{
"files": {
"tokenharbor.json": "{\"slug\": \"menu-reader\", \"name\": \"Menu Reader\"}",
"index.html": "<!doctype html>β¦",
"css/style.css": "body { margin: 0 }",
"assets/logo.png": { "base64": "iVBORw0KGgoβ¦" }
}
}
Each path is relative to the app's folder. Text files are strings; pictures, fonts and sounds are { "base64": "β¦" }. The whole request can be up to about 4 MB.
201 means the app was created, 200 that it was updated, and both return "ok": true with the app, its new version, its url, any warnings and skipped files. 422 is a refused folder, as above. 409 with the code slug_taken means the address belongs to another app: choose another slug. Both carry problems in the same shape, so the loop above handles them alike.