Artifact Capabilities

A page is static until it asks. Each capability buys exactly the reach it names, and nothing else.

Declare them at publish time:

bash
1belt artifact publish ./poll.html --capability db --capability user

A page gets only what its published version declared, so granting one is a deliberate act rather than something that accumulates. An unknown name is refused at publish rather than stored.

Every call goes out through the viewer's own session, so a page can only ever act as the person looking at it. That is what makes handing one to a stranger safe.


db — the page remembers

js
1await window.inferencesh.db.set("votes", "abc", { choice: "green" })2const doc  = await window.inferencesh.db.get("votes", "abc")3const docs = await window.inferencesh.db.list("votes")4await window.inferencesh.db.update("votes", "abc", { seen: true })5await window.inferencesh.db.delete("votes", "abc")

A document is { collection, doc_id, data, updated_at } — the values you stored are under .data. db.list resolves to an array of every document in the collection, newest first, and to an empty array when the collection is empty.

Listing. db.list takes { limit, order, where }:

js
1const red    = await window.inferencesh.db.list("votes", { where: { choice: "red" } })2const oldest = await window.inferencesh.db.list("votes", { order: "created_at", limit: 10 })
  • order is -updated_at (the default), updated_at, -created_at or created_at.
  • where keeps documents whose top-level fields equal the values given: a string, number or boolean, or null for a field that is missing.
  • limit stops after that many documents.

db.list fetches page after page until the collection ends. To read one page at a time, use db.page, which returns at most 200 documents and the cursor of the next page:

js
1let cursor = null2do {3  const page = await window.inferencesh.db.page("votes", { cursor })4  render(page.documents)5  cursor = page.next_cursor6} while (cursor)

Two namespaces. Documents under data/users/me are private to the viewer who wrote them — me resolves server-side, so a page can only ever name the person currently looking at it, and that subtree is private even from the artifact's author. Every other collection is shared with everyone who can open the page.

Reads are open, shared writes need interact. Anyone who can open the page can read shared documents. Writing one needs interact on the page: its workspace, the people it is shared with, and its org's members when it is org-visible. A public or unlisted link alone gives read, so such a viewer is refused on a shared write. Any signed-in viewer can write their own data/users/me documents; a signed-out viewer has none. Handle the rejection — every call returns a promise that can reject.

Limits. 128 KiB per document, 16 MiB per artifact, 10,000 documents. One page holds at most 200, and where may name up to 8 fields.

A shared collection has no per-document rules: anyone with interact may write any document id in it, including one another's. data/users/me is the part that is enforced per person. If a page depends on one-row-per-person, treat that as a convention it follows rather than a rule the store keeps.


user — who is looking

js
1const v = await window.inferencesh.user.get()2// { signed_in, user_id, name, avatar_url, can_edit }

It never carries an email or a credential. avatar_url is loadable in an <img> — declaring this capability is what admits it through the CSP — and is absent for a viewer with no picture, so fall back to an initial.


assets — files stored beside the page

Upload with the CLI:

bash
1belt artifact asset upload acme/q3-funnel ./chart.png2belt artifact asset list acme/q3-funnel

The page reads a manifest baked into the document at render time — nothing is fetched to obtain it:

js
1window.inferencesh.assets2// [{ asset_id, filename, content_type, size_bytes, url }, …]

Use each url exactly as given. Asset bytes are not on the public upload CDN; they are fetched through the viewer after the same permission check as the page itself, so a private artifact's images stay private — and an address you construct yourself will not load.

Declaring assets widens only the image, media and font directives of the policy. The page still cannot fetch anything.

Limits. 25 MiB per file, 250 MiB per artifact, 500 files.


How a call actually travels

The page has no credential and no network. window.inferencesh posts a message to the viewer that embeds it, and the viewer makes the request with the session of whoever is looking:

code
1page  ──postMessage──▶  viewer (your session)  ──https──▶  api

So a page cannot read anything you could not read yourself, and cannot write anything you could not write yourself.

In an MCP host the viewer is one of the inference.sh MCP server's cards. It nests the page and makes each call as a page-only MCP tool. The page sees no difference.


Pages that are an app's UI

An app version can name an artifact as its UI. The page then gets the run it is showing from its host:

js
1window.inferencesh.tool.input    // the run's input2window.inferencesh.tool.result   // the run's result, once there is one3window.addEventListener("inferencesh:tool-result", (e) => render(e.detail))

inferencesh:tool-input fires the same way. window.inferencesh.host.theme and the inferencesh:host-context event say which theme the host is in.

we use cookies

we use cookies to ensure you get the best experience on our website. for more information on how we use cookies, please see our cookie policy.

by clicking "accept", you agree to our use of cookies.
learn more.