# Artifact Capabilities

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

**URL:** https://inference.sh/docs/artifacts/capabilities
**Last updated:** 2026-10-25

---

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

Declare them at publish time:

```bash
belt 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
await window.inferencesh.db.set("votes", "abc", { choice: "green" })
const doc  = await window.inferencesh.db.get("votes", "abc")
const docs = await window.inferencesh.db.list("votes")
await window.inferencesh.db.update("votes", "abc", { seen: true })
await 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
const red    = await window.inferencesh.db.list("votes", { where: { choice: "red" } })
const 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
let cursor = null
do {
  const page = await window.inferencesh.db.page("votes", { cursor })
  render(page.documents)
  cursor = page.next_cursor
} 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
const v = await window.inferencesh.user.get()
// { 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
belt artifact asset upload acme/q3-funnel ./chart.png
belt 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
window.inferencesh.assets
// [{ 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:

```
page  ──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
window.inferencesh.tool.input    // the run's input
window.inferencesh.tool.result   // the run's result, once there is one
window.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.
