A page is static until it asks. Each capability buys exactly the reach it names, and nothing else.
Declare them at publish time:
1belt artifact publish ./poll.html --capability db --capability userA 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
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", { limit: 200 })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 them, newest first, and to an empty array when the collection is empty.
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, writes need an account. Someone viewing a public page while signed out can read shared documents and will be refused on write. Handle the rejection — every call returns a promise that can reject.
Limits. 128 KiB per document, 16 MiB per artifact, 10,000 documents. A list returns at most 200.
Today a shared collection has no write rules: any signed-in viewer who can read the page may write any document id in it, including one another's.
data/users/meis the part that is enforced. 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
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:
1belt artifact asset upload acme/q3-funnel ./chart.png2belt artifact asset list acme/q3-funnelThe page reads a manifest baked into the document at render time — nothing is fetched to obtain it:
1window.inferencesh.assets2// [{ asset_id, filename, content_type, size_bytes, url }, …]Use each url exactly as given. Assets are served from the page's own origin 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:
1page ──postMessage──▶ viewer (your session) ──https──▶ apiSo a page cannot read anything you could not read yourself, and cannot write anything you could not write yourself.