---
name: iqobi
description: Publish apps to iqobi and share them with your friends and the world
version: v0.0.1
homepage: https://iqobi.com
---

# iqobi

iqobi hosts apps that you create and share.

The main flow:

1. The person signs in once on https://iqobi.com.
2. You write the app's files.
3. You upload them, and iqobi checks that the app renders.
4. When the person says yes, you publish. The app goes live at a URL like
   `<app>.<username>.iqobi.app`.
5. To change the app, upload new files and publish again.

## Keep this skill current

Save this skill in your setup, with the `Iqobi-Skill-Identity` response
header that came with it. That header is the SHA-256 of this file. Before you
start, fetch `https://api.iqobi.com/skill.md`. If the header differs from the one you
saved, replace your copy.


## What you can publish

Any kind of app: games, tools, and more. It must not cause harm or infringe
copyright. When a file is broken, iqobi says what to fix.

## API

The public API is at `https://api.iqobi.com`. Every route, schema, and status is in
`https://api.iqobi.com/v1/openapi.json`. Use only the routes in this file.

```sh
POST /oauth/device                                # a link for the person
POST /oauth/token   -d device_code=…              # your token
POST /v1/apps       -F name=… -F files=@app.js    # a job
GET  /v1/jobs/{job_id}                            # when done, the app
POST /v1/apps/{app_id}/publish                    # the review, then publish
```

## 1. Sign in

iqobi uses OAuth 2.0. You do not need a password or API key.

### Ask for a sign-in link

```sh
curl -sS -X POST "https://api.iqobi.com/oauth/device"
```

Add `-d label="…"` to name yourself on the approval page, at most
80 characters.

```json
{
  "device_code": "…",
  "user_code": "HJ88-TC5B",
  "verification_uri": "https://iqobi.com/device",
  "verification_uri_complete": "https://iqobi.com/device?code=HJ88-TC5B",
  "expires_in": 300,
  "interval": 5
}
```

Show the person `verification_uri_complete`. They sign in and approve. Keep
`device_code` to yourself.

### Wait for the token

Every `interval` seconds, ask for the token:

```sh
curl -sS -X POST "https://api.iqobi.com/oauth/token" \
  -d device_code="$DEVICE_CODE"
```

- `authorization_pending`: ask again.
- `slow_down`: wait 5 seconds longer between requests.
- `expired_token` or `access_denied`: stop.

Once the person approves, you get the token:

```json
{
  "access_token": "…",
  "expires_in": 899,
  "refresh_token": "…",
  "scope": "account:read apps:create apps:publish library:read studio:read studio:write",
  "token_type": "Bearer"
}
```

Send `Authorization: Bearer $IQOBI_TOKEN` to `https://api.iqobi.com` and nowhere else.
If `scope` lacks `apps:publish`, the person turned off publishing. Do not
publish.

### Refresh the token

When a call answers `not_authenticated`, refresh:

```sh
curl -sS -X POST "https://api.iqobi.com/oauth/token" \
  -d refresh_token="$REFRESH_TOKEN"
```

**Save the new `refresh_token` each time.** The old one stops working.
`invalid_grant` means the person must sign in again.


## 2. Write the files

Use your own HTML, CSS, and JavaScript. The iqobi SDK is optional.

Include one browser entry, `app.js` or `app.ts`. iqobi compiles TypeScript to
JavaScript. Compile JSX and TSX before you upload.

Add `app.css` for styles. iqobi supplies the page and loads both files.

You may add other modules, stylesheets, and text files in folders. Refer to
them with relative paths. Apps run in iqobi's sandbox, without arbitrary
network access or npm installs at run time.

This `app.js` is a complete upload:

```js
const root = document.getElementById("app");
const button = document.createElement("button");
let count = 0;
button.textContent = "Count: 0";
button.addEventListener("click", () => {
  count += 1;
  button.textContent = `Count: ${count}`;
});
root?.append(button);
```

Limits:

- Text files only: no media files such as `.png`, no `node_modules`, and at
  most 8 KiB of inline `data:` media.
- At most 400 files, 256 KiB each, 10 MiB in all.
- No secrets: no API keys, tokens, or passwords.

### What the check refuses

Write the files so that the first upload passes:

- Leave out `index.html`. iqobi writes one that loads `app.css` and
  `app.js`. A custom one needs the doctype, `html`, `head`, and `body` in
  order, no script in `head`, and only `<script type="module">`.
- Import `react`, `react-dom/client`, `react/jsx-runtime`, `@iqobi/ui`,
  `@iqobi/sdk/runtime`, `@iqobi/sdk/react`, `@iqobi/sdk/integrations`, or
  relative files.
- If `app.js` imports `@iqobi/sdk/...`, it must claim the page: call
  `mount(root)` from `@iqobi/sdk/runtime`, or `createRoot(root).render(...)`.
- No emoji or pictographic symbols in any file, including check marks,
  crosses, and stars (U+2600 to U+27BF, U+2B00 to U+2BFF, and U+FE0F).
  Use words, or an `Icon` from `@iqobi/ui`.
- Build the page with React or DOM methods such as `createElement` and
  `textContent`. HTML parsing is refused: `innerHTML`, `outerHTML`,
  `insertAdjacentHTML`, `document.write`, and `execCommand`.
- No `alert`, `confirm`, or `prompt`. Use a `Dialog` from `@iqobi/ui` or an
  inline message.
- The app stays in its frame: no `location.href`, `location.assign`,
  `location.replace`, `window.open`, or nested frames. Route inside the app.
- No network calls (`fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`),
  no `eval`, and no `document.cookie`.
- No fixed width above 400px, unless it sits in a `min-width` media query
  at least that wide. Apps must fit a 358px phone.
- iqobi type-checks JavaScript. Narrow a DOM lookup before you read its
  properties, for example `if (input instanceof HTMLInputElement)` before
  `input.value`.
- No fields for card numbers, passwords, or security codes. The check
  matches words anywhere in `.js` and `.html` files, so avoid "credit card",
  "card number", "cvv", "security code", "verification code", and "one-time
  code" even in labels.

### Save shared data and sign people in

Browser storage stays on one device. To share data between the people who
use the app, add `schema.json` and `server.js`:

```json
{"tables":{"notes":{"fields":{"text":{"type":"text","required":true,"max":200}}}}}
```

```js
export const queries = {
  listNotes: {
    description: "Read notes",
    input: {type: "object", properties: {}, additionalProperties: false},
    mcp: false,
    run(args, ctx) {
      return ctx.data.table("notes").list({orderBy: "createdAt desc", limit: 50});
    },
  },
};

export const commands = {
  addNote: {
    description: "Save a note",
    input: {
      type: "object",
      properties: {text: {type: "string", minLength: 1, maxLength: 200}},
      required: ["text"],
      additionalProperties: false,
    },
    mcp: false,
    run(args, ctx) {
      return ctx.data.table("notes").insert({text: args.text});
    },
  },
};

export const owner = {};
```

`server.js` has no imports and exports only literal `queries`, `commands`,
and `owner` objects, all three. Each entry has `description` (at most 200
characters), `input` (a JSON Schema literal whose root is
`type: "object"`), `mcp` (`true` or `false`), and `run(args, ctx)`, and no
other key except an optional `share` on commands. `run` is
synchronous and returns JSON: an `async` run is refused. Queries cannot
write. Each command or owner call is one transaction.

`ctx.data.table(name)` gives `list`, `count`, `insert`, `upsert`, `update`,
`delete`, and `deleteMany`. `list({where, orderBy, limit, after})` returns
`{rows, total, nextCursor}`, at most 200 rows a page. Rows get `id`,
`createdAt`, `updatedAt`, `createdBy`, and `updatedBy`.

In the browser, `useQuery("listNotes", args)` from `@iqobi/sdk/react`
returns `{value, rows, total, loading, error}` and refreshes after each
successful write. `await server.call("addNote", {text})` from
`@iqobi/sdk/runtime` runs a command or an owner function. Give `server.call`
the function name as a string literal, never a variable.

`session.user()` from `@iqobi/sdk/runtime` is `null` or
`{userId, name, isCreator, permission}`. `session.signIn()` asks a visitor
to sign in. On the server, `ctx.user` is `null` or
`{userId, isCreator, permission}`. Trust it, never an identity sent in
arguments. A viewer calls queries, an editor commands, and only the owner
`owner` functions. Visitors read data only through your queries, so return
only what each caller may see. People who add their own data must be
editors. The owner shares the app on https://iqobi.com.

## 3. Upload

Create the app. `name` is 1 to 60 characters, without `-`.

```sh
curl -sS -X POST "https://api.iqobi.com/v1/apps" \
  -H "Authorization: Bearer $IQOBI_TOKEN" \
  -F name="Counter" \
  -F files=@app.js
```

Send one `files` part per file. For a file in a folder, give its path:
`-F "files=@lib/store.js;filename=lib/store.js"`. Add `-F summary="…"` to say
what changed.

To upload a new version, send `app_id` instead of `name`:

```sh
curl -sS -X POST "https://api.iqobi.com/v1/apps" \
  -H "Authorization: Bearer $IQOBI_TOKEN" \
  -F app_id="$APP_ID" \
  -F files=@app.js
```


The answer is a job:

```json
{
  "job_id": "job_…",
  "status_url": "https://api.iqobi.com/v1/jobs/job_…",
  "poll_after_seconds": 2
}
```

## 4. Wait for the job

```sh
curl -sS "https://api.iqobi.com/v1/jobs/$JOB_ID" \
  -H "Authorization: Bearer $IQOBI_TOKEN"
```


Ask again every `poll_after_seconds` until `state` is `done` or `failed`.
A `done` job carries `result.app_id` and `result.version`. A
`failed` job says what to fix:

```json
{
  "job_id": "job_…",
  "kind": "create",
  "state": "failed",
  "app_id": "",
  "poll_after_seconds": 0,
  "result": null,
  "error": {
    "code": "package_refused",
    "message": "…",
    "diagnostics": [
      {
        "code": "checkjs.property",
        "file": "app.js",
        "message": "line 1, column 8: Property 'missing' does not exist on type 'HTMLButtonElement'."
      }
    ],
    "truncated": false,
    "skill_identity": "…"
  }
}
```

Fix the files and upload again.

## 5. Publish


**Publish only after the person says yes, every time.**

```sh
curl -sS -X POST "https://api.iqobi.com/v1/apps/$APP_ID/publish" \
  -H "Authorization: Bearer $IQOBI_TOKEN" \
  -H "Content-Type: application/json"
```


The review names the app, `draft_version_id`, `version_number`, `summary`,
`live_url`, and the current audience. Its `access` is
`private`, `shared`, or `public`. Show the person the version, access,
`public_role`, `member_count`, and the link role when `link_enabled` is true.
When they approve that review, send its `review_token` back:

```sh
curl -sS -X POST "https://api.iqobi.com/v1/apps/$APP_ID/publish" \
  -H "Authorization: Bearer $IQOBI_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"review_token\": \"$REVIEW_TOKEN\"}"
```


The answer has the live `url`. `review_stale` means something changed since
the review. Send the publish request again without `review_token` to get a new
review, show it, and ask again. A first publish keeps the app private. The
person shares it on https://iqobi.com.

## Roll back

To put back a version that was live before:

```sh
curl -sS -X POST "https://api.iqobi.com/v1/apps/$APP_ID/rollback" \
  -H "Authorization: Bearer $IQOBI_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"version_id\": \"$VERSION_ID\"}"
```


## Errors


```json
{
  "error": {
    "code": "not_found",
    "message": "…",
    "retryable": false,
    "request_id": "…"
  }
}
```

| Code | Status | Do this |
|---|---|---|
| `invalid_request` | 400 | Fix the request as `message` says. |
| `package_refused` | 400 | Fix what `diagnostics` names and send the files again. On publish or rollback, choose or push another version. |
| `not_authenticated` | 401 | Refresh the token once and retry. If the refresh answers `invalid_grant`, sign in again. |
| `insufficient_scope` | 403 | This sign-in lacks the scope. Ask the person to sign you in again and allow it. |
| `not_found` | 404 | The app, version, job, draft, or address does not exist, or this account cannot reach it. |
| `draft_changed` | 409 | The draft changed while the job ran. Read the app and upload again. |
| `schema_conflict` | 409 | The version's `schema.json` cannot read the app's current data. Change it and push, or on rollback choose another version. |
| `review_stale` | 409 | Read the review again and show it to the person again. |
| `conflict` | 409 | The app's address is taken. Try another name. |
| `create_pending` | 409 | The create job is still running. Wait for `Retry-After`, then push. |
| `payload_too_large` | 413 | Send fewer or smaller files. See Package rules. |
| `rate_limited` | 429 | Wait for `Retry-After`, then send the same request. |
| `quota` | 429 | With `Retry-After`, wait and send again. Without it, waiting does not help: the package has too much inline media, the account too many apps, or the app too many versions. See Limits. |
| `billing.upgrade_required` | 402 | The owner's plan does not allow this. Tell the person; retrying does not help. |
| `compiler_busy` | 503 | The checker is busy. Wait, then send the same request. |
| `unavailable` | 503 | Wait, then send the same request, unless `message` says this server cannot run apps. |

When an answer carries `Retry-After`, wait that many seconds and send the
request again.

