# What can my page do?

Everything a published page can do beyond showing things. People don't need these names: they say what they want ("collect answers", "let people vote") and their AI picks from this list. These pages are for AI agents and power users.

| Capability | What it does | When to use it | Status |
| --- | --- | --- | --- |
| [Database](/docs/capabilities/database) | Every space has its own SQLite database, shared by all its pages, so a page keeps data between visits. | The page keeps data between visits: lists, scores, answers, a tracker. Members add or edit it; everyone can read it. | Live |
| [Guest writes](/docs/capabilities/guest-writes) | Visitors who aren't signed in add data, through statements you publish: sign-ups, votes, feedback, scores. | Visitors who aren't signed in add data: a sign-up sheet, a vote, feedback, a high score. Save it in the database. | Live |
| [Space files](/docs/capabilities/files) | People add files to a space for the work: chat exports, emails, PDFs, spreadsheets, screenshots. | The work needs files people give you: exports, PDFs, spreadsheets, images. | Live |
| [Comments](/docs/capabilities/comments) | Anyone who can open the page can comment on it, even on a quote from it. Built in. | Feedback on the page itself. It's built in: don't build a comment form. | Live |
| [Realtime rooms](/docs/capabilities/realtime-rooms) | People with the page open join a room by name and see each other act, live: messages, who's here, a host, and a small shared state. | Several people have the page open at once and what one does should show up for the others within a second: a live poll, planning poker, a quiz, cursors, a game. Save results that should last in the database. | Preview |
| [Decisions](/docs/capabilities/decisions) | The page asks an AI model to judge text or pictures and gets a typed answer back: a choice or a score. | The page needs a judgement it can't compute: pick the best match, score an answer, check a photo. | Preview, per space |

## Pick by need

- keep data between visits → database
- visitors without an account add data → guest writes (save it in the database)
- files people give you → space files
- feedback on the page → comments (built in)
- an AI judgement inside the page → decisions (preview, per space)

**What a page can't do:** No video or audio files, no sending email, no server code of your own. Pages can call outside services with your own API keys.

## For agents

Every page here has a Markdown twin: https://cloudtifact.com/capabilities.md has all of them in one file, and /capabilities/<name>.md has one. `cloudtifact capabilities --space <id>` and the MCP server's `list_capabilities` tool list them with whether each is on in a space.

- Database: https://cloudtifact.com/capabilities/database.md
- Guest writes: https://cloudtifact.com/capabilities/guest-writes.md
- Space files: https://cloudtifact.com/capabilities/files.md
- Comments: https://cloudtifact.com/capabilities/comments.md
- Realtime rooms: https://cloudtifact.com/capabilities/realtime-rooms.md
- Decisions: https://cloudtifact.com/capabilities/decisions.md

When a page calls something that's off for its space, the call rejects with code `capability_off`, and the message says who can turn it on.

## Database

Every space has its own SQLite database, shared by all its pages, so a page keeps data between visits.

**When to use it:** The page keeps data between visits: lists, scores, answers, a tracker. Members add or edit it; everyone can read it.

**Status:** Live. **Turned on:** Always on. Visitors can read it while "Pages can show saved data" is on in the space's Settings (the default for public spaces).

### How

Schema and seed data go in db/NNN_name.sql next to the page and run on publish. In the page, cloudtifact.db.query(sql, params) reads and cloudtifact.db.exec(sql, params) writes.

```sql
-- db/001_schema.sql, next to the page. Runs once, on publish.
CREATE TABLE IF NOT EXISTS ideas (id INTEGER PRIMARY KEY, text TEXT NOT NULL, votes INTEGER DEFAULT 0);
```

```html
<ul id="ideas"></ul>
<script>
  (async () => {
    const { rows } = await cloudtifact.db.query(
      "SELECT text, votes FROM ideas ORDER BY votes DESC", [], { objects: true });
    document.getElementById("ideas").innerHTML =
      rows.map((r) => `<li>${r.text} (${r.votes})</li>`).join("");
  })();
  // Members viewing the page in the Cloudtifact app can write:
  // await cloudtifact.db.exec("INSERT INTO ideas (text) VALUES (?)", [text]);
</script>
```

### Limits

- Writes from the page need a signed-in member viewing it in the Cloudtifact app; a direct link is read-only. For visitors, use guest writes.
- Use ? placeholders and pass values in params; never build SQL from what people type.
- Visitors read the whole database while the space lets pages show saved data (on by default for public spaces), so don't keep secrets in it.

### Errors

A failed call rejects with an Error whose `code` is one of:

| Code | When |
| --- | --- |
| `read_only` | exec() without write access: a direct link, or a visitor. Handle it; don't retry. |
| `forbidden` | Visitors can't read this space's database (the space turned that off). |
| `bad_request` | SQLite refused the statement; the message has its reason. |
| `invalid` | The arguments are wrong (sql must be a string, params an array). |
| `network` | The request didn't reach Cloudtifact (offline, blocked). |
| `timeout` | No answer in time; try again. |

### Cost

Included in every plan.

## Guest writes

Visitors who aren't signed in add data, through statements you publish: sign-ups, votes, feedback, scores.

**When to use it:** Visitors who aren't signed in add data: a sign-up sheet, a vote, feedback, a high score. Save it in the database.

**Status:** Live. **Turned on:** Off by default. An admin turns on "Pages can save data for people who are not signed in" in the space's Settings, under Guests. Public spaces only.

### How

Publish named statements in db/guest-writes.sql ("-- name: vote", then one INSERT, UPDATE or DELETE; $guest and $guest_id are filled in by the server). In the page, hide the form when `(await cloudtifact.ready).guestWrites === false`; `null` means try. Then cloudtifact.guest.set(name) once and cloudtifact.db.run("vote", params) on submit.

```sql
-- db/guest-writes.sql, next to the page. Publishing replaces the whole set.
-- name: vote
INSERT INTO votes (idea_id, voter, bucket) SELECT ?, $guest, substr($guest_id, 1, 4)
  WHERE (SELECT count(*) FROM votes WHERE bucket = substr($guest_id, 1, 4)) < 3;
```

```html
<form id="vote" hidden><button name="idea" value="1">Vote</button></form>
<script>
  async function vote(ideaId, name) {
    const { guestWrites } = await cloudtifact.ready;
    if (guestWrites === false) return;
    await cloudtifact.guest.set(name);
    await cloudtifact.db.run("vote", [ideaId]);
  }
  cloudtifact.ready.then(({ guestWrites }) => {
    if (guestWrites === false) return;
    document.getElementById("vote").hidden = false;
  });
</script>
```

### Limits

- Public spaces only; off until an admin turns it on.
- 120 writes per browser and 300 per address every 10 minutes.
- Params are strings up to 4 KB, numbers, booleans or null. Every write is logged with who and roughly where from.
- Visitors can read every table, so never store $guest_id itself; keep a short prefix if you need a bucket for caps.

### Errors

A failed call rejects with an Error whose `code` is one of:

| Code | When |
| --- | --- |
| `no_guest` | Call cloudtifact.guest.set(name) first. |
| `forbidden` | Guest writes are off for this space, or the page was opened by a private link (no guests there). The bridge refuses it before the page tries. Hide the form when ready's guestWrites is false; unanswered (null) still try. |
| `rate_limited` | Too many writes from this browser or address; wait a few minutes. |
| `bad_request` | No statement with that name, or the params don't fit it. |
| `network` | The request didn't reach Cloudtifact (offline, blocked). |
| `timeout` | No answer in time; try again. |

### Cost

Included in every plan.

## Space files

People add files to a space for the work: chat exports, emails, PDFs, spreadsheets, screenshots.

**When to use it:** The work needs files people give you: exports, PDFs, spreadsheets, images.

**Status:** Live. **Turned on:** Always on.

### How

People upload in the app (or with cloudtifact upload). Agents list them with cloudtifact sources and fetch one with cloudtifact download --source.

```sh
cloudtifact sources --space <id> --toon
cloudtifact download --source "Q3 numbers.xlsx" --space <id>
```

### Limits

- Agents can read files up to 25 MB.
- Uploaded files are downloads, not pages. No video or audio playback in pages.

### Cost

Included in every plan.

## Comments

Anyone who can open the page can comment on it, even on a quote from it. Built in.

**When to use it:** Feedback on the page itself. It's built in: don't build a comment form.

**Status:** Live. **Turned on:** Always on for people who can open the space. Guests: "People who are not signed in can comment" in the space's Settings.

### How

Nothing to add. A public page has a Comment button, and ?comments in its link opens the panel. Agents read and answer with cloudtifact comments, reply and resolve.

```sh
cloudtifact comments --space <id> --toon
cloudtifact reply <commentId> --space <id> "Fixed: the totals now include March"
cloudtifact resolve <commentId> --space <id>
```

### Limits

- People who aren't signed in comment only on public spaces with "People who are not signed in can comment" on.

### Cost

Included in every plan.

## Realtime rooms

People with the page open join a room by name and see each other act, live: messages, who's here, a host, and a small shared state.

**When to use it:** Several people have the page open at once and what one does should show up for the others within a second: a live poll, planning poker, a quiz, cursors, a game. Save results that should last in the database.

**Status:** Preview. **Turned on:** On for every space. The space's admins can turn it off in its Settings, under What pages can do.

### How

const room = await cloudtifact.rooms.join(name, { name: displayName }) joins (names: 1 to 40 letters, digits, - or _). room.send(data) reaches everyone else, room.send(data, { to: memberId }) one person; room.on("message", (data, from) => ...). room.members, room.me and room.on("members") say who's here; room.host (room.on("host")) is always someone connected, so one browser can run a quiz or game for everyone. room.set(key, value) and room.on("state") keep shared state that latecomers get. cloudtifact.rooms.count(name) and rooms.list() read live counts without joining.

```html
<p id="who"></p><button id="yes">Yes</button><p id="tally"></p>
<script>
  (async () => {
    const room = await cloudtifact.rooms.join("all-hands", { name: "Mia" });
    const show = () => {
      who.textContent = room.members.filter((m) => !m.away).length + " here";
      // One key per person, so votes cast at the same moment never overwrite each other.
      const yes = Object.keys(room.state).filter((k) => k.startsWith("vote:") && room.state[k] === "yes");
      tally.textContent = yes.length + " said yes";
    };
    room.on("members", show);
    room.on("state", show);
    document.getElementById("yes").onclick = () => room.set("vote:" + room.me.id, "yes");
    show();
  })();
</script>
```

### Limits

- 16 people a room; 100 rooms with people in them and 500 people in rooms per space.
- 30 messages a second per person (send game state 10 to 20 times a second, not every frame); 16 KB a message.
- Shared state: 128 keys, 4 KB a value, 256 KB a room; kept 7 days after the room empties. Save what should last in the database.
- Anyone who can open the page can join its rooms by name, so use a random code for a private session. Members-only spaces' rooms open only through their links.
- It reconnects by itself and keeps the person's id for 15 seconds; sends while reconnecting return false and are dropped.

### Errors

A failed call rejects with an Error whose `code` is one of:

| Code | When |
| --- | --- |
| `room_full` | The room already has 16 people. |
| `space_limit` | The space has too many rooms or people in rooms right now; try later. |
| `rate_limited` | Over 30 messages a second; some were dropped (an "error" event, not a rejection). |
| `too_large` | A message or state value is over its size limit (an "error" event). |
| `capability_off` | The space's admins turned realtime rooms off. |
| `forbidden` | The page's link is invalid or expired, or the space needs its password. |
| `unsupported` | Not a published Cloudtifact page, or no WebSocket in this browser. |
| `invalid` | The room name isn't 1 to 40 letters, digits, - or _. |
| `network` | Couldn't reach the room after a few tries. |

### Cost

Included in every plan, with the limits above.

## Decisions

The page asks an AI model to judge text or pictures and gets a typed answer back: a choice or a score.

**When to use it:** The page needs a judgement it can't compute: pick the best match, score an answer, check a photo.

**Status:** Preview. **Turned on:** Invite-only preview: Cloudtifact turns it on for a space, then the space's admins can switch it off and on in Settings.

### How

await cloudtifact.decide({ state, questions, assets? }) resolves to { answers, model, usage }. Each question is { type: "choice" | "score" | "noul", instructions, criteria? }; images go in assets as { format: "png" | "jpg", data_base64 } and are referenced by name.

```html
<script>
  async function judge(jpegBase64) {
    const { answers } = await cloudtifact.decide({
      state: { photo: "photo-1" },
      assets: { "photo-1": { format: "jpg", data_base64: jpegBase64 } },
      questions: {
        sign: { type: "choice", instructions: "Which hand sign is in `photo`?",
                criteria: { tiger: "Tiger", ram: "Ram" } },
      },
    });
    return answers.sign.choice; // "tiger"
  }
</script>
```

### Limits

- Turned on per space while it's a preview.
- 120 calls a minute per visitor and 1,200 per space.
- 4 MB a request, 32 questions and 16 images; images at least 280 px a side.

### Errors

A failed call rejects with an Error whose `code` is one of:

| Code | When |
| --- | --- |
| `capability_off` | Not turned on for this space. The message says who can turn it on. |
| `rate_limited` | Too many calls this minute, from this browser or the whole space. |
| `bad_request` | The request's shape is wrong, or the model refused it; the message says why. |
| `too_large` | Over 4 MB. |
| `upstream_error` | The model service failed; try again. |
| `invalid` | decide() wasn't given an object. |
| `network` | The request didn't reach Cloudtifact (offline, blocked). |
| `timeout` | No answer in time; try again. |

### Cost

Each call is an AI request we pay for, so it's invite-only while it's a preview. Plans will include an allowance later.
