Collaborative documents and little apps your agent can build, inspect and maintain.
Your agent (Claude, Codex, Cursor or any MCP client) writes MDX or React over MCP. People open a link, sign in with an emailed code, and get a live page: shared state, per-person state, presence, comments, approvals and forms built in, with no backend to wire up. What they decide comes back to the agent. Everyone it isn't shared with never sees it.
Free to start, no card needed.
What the agent writes, and what people open
Three real artifacts, compiled and rendered by the same code as any other. Vote, tick and type in them: nothing leaves your browser.
# Launch plan: billing v2
<Summary>Ship **this Thursday** unless the load test fails. Vote on the day below.</Summary>
<Stats>
<Stat label="Beta signups" value="1,284" delta="+18%" />
<Stat label="Open bugs" value="7" delta="-5" invert />
<Stat label="p95 checkout" value="420ms" delta="-12%" invert />
</Stats>
```mermaid
flowchart LR
Freeze[Code freeze] --> Load[Load test] --> Ship
Load -- fails --> Fix[Fix and retest] --> Load
```
<Chart type="area" data="signups.json" x="week" y="signups" height={200} />
## Decide
<Poll id="day" question="When do we launch?" options={["This Thursday", "Next Thursday"]} />
<Approval id="go" title="Go for launch" description="Sign off once your part is ready." approvers={["priya.shah@example.com", "marcus.lee@example.com", "tom.okafor@example.com"]} />
<Checklist id="launch" items={["Docs", "Status page", "Announcement email"]} />
<Comments />
MDX with a diagram, a chart, and a poll, sign-off, checklist and comments that everyone shares. Open it as a public page.
import { useState } from "react";
import { Avatar, Input } from "@a/ui";
import { useSharedMap, useViewer } from "@a/hooks";
import { ThumbsUp } from "lucide-react";
type Card = { col: string; text: string; by: string | null };
const COLUMNS = ["Went well", "To improve", "Actions"];
export default function Retro() {
const cards = useSharedMap<Card>("card");
const votes = useSharedMap<boolean>("vote"); // vote/<card>/<email>
const { email, canWrite } = useViewer();
const [drafts, setDrafts] = useState<Record<string, string>>({});
const count = (id: string) => votes.entries.filter(([k]) => k.startsWith(`${id}/`)).length;
const mine = (id: string) => votes.get(`${id}/${email}`);
function add(col: string) {
const text = drafts[col]?.trim();
if (!text || !canWrite) return;
cards.set(crypto.randomUUID(), { col, text, by: email });
setDrafts({ ...drafts, [col]: "" });
}
function vote(id: string) {
const key = `${id}/${email}`;
mine(id) ? votes.delete(key) : votes.set(key, true);
}
return (
<div className="grid min-h-full content-start gap-4 px-5 pt-5 pb-20 sm:grid-cols-3">
{COLUMNS.map((col) => (
<section key={col} className="flex flex-col gap-2">
<h2 className="text-sm font-semibold text-muted-foreground">{col}</h2>
{cards.entries
.filter(([, c]) => c.col === col)
.sort((a, b) => count(b[0]) - count(a[0]))
.map(([id, c]) => (
<div key={id} className="flex items-start gap-2 rounded-lg border border-border bg-card p-3 text-sm">
<Avatar email={c.by ?? undefined} size="sm" />
<span className="flex-1">{c.text}</span>
<button
className={`flex items-center gap-1 text-xs ${mine(id) ? "text-primary" : "text-muted-foreground"}`}
disabled={!canWrite}
onClick={() => vote(id)}
>
<ThumbsUp className="size-3.5" /> {count(id)}
</button>
</div>
))}
<Input
placeholder="Add a card…"
value={drafts[col] ?? ""}
disabled={!canWrite}
onChange={(e) => setDrafts({ ...drafts, [col]: e.target.value })}
onKeyDown={(e) => e.key === "Enter" && add(col)}
/>
</section>
))}
</div>
);
}
A React app in one file: cards and votes sync live between everyone with the page open. Open it as a public page.
# Cloud costs: September
<Summary>Spend fell **14%** to $18.2k after CI moved to spot instances.</Summary>
<Stats>
<Stat label="Total" value="$18.2k" delta="-14%" invert />
<Stat label="Compute" value="$11.0k" delta="-22%" invert />
<Stat label="Storage" value="$4.1k" delta="+6%" invert />
</Stats>
<Chart type="bar" data="monthly.json" x="month" y="compute storage network" stacked height={220} />
<Table
data="services.json"
columns={["service", "team", { key: "cost", format: "currency" }, { key: "change", format: "percent" }]}
sort="-cost"
search
/>
<Callout tone="warning" title="Storage keeps growing">
Logs are kept for 90 days. Keeping 30 would save about $900 a month.
</Callout>
## October budgets
<Form
id="budget"
title="Your team's October budget"
description="One answer per team lead. You can change yours until the 25th."
fields={[
{ name: "team", label: "Team", type: "select", options: ["Platform", "Search", "Data", "Web"], required: true },
{ name: "budget", label: "Budget ($)", type: "number", required: true },
{ name: "logs", label: "Cut log retention to 30 days", type: "checkbox" },
]}
/>
<Notes id="followups" placeholder="Follow-ups for next month…" rows={3} />
A report from data files: sortable table, stacked chart, a budget form and shared notes. Open it as a public page.
Live pages, not static HTML
State, people and data are part of the page, not something to wire up.
useShared,useSharedMapanduseLocalsync over a WebSocket;useViewerandusePresenceknow who's looking- Polls, approvals, forms, checklists, notes and comments work out of the box, alongside tables, charts and Mermaid diagrams
- A data file can refresh from a JSON URL on a schedule, once a person approves the host
An edit-and-check loop for agents
Agents change artifacts the way they change code, and find out when they broke something.
- Ranged reads, grep, exact find-and-replace edits and JSON Patch, with version guards against stale writes
- Imports, components and syntax validated before anything is saved
- Every write rendered in a headless browser; runtime errors and a screenshot come back in the tool result
People answer, agents close the loop
An artifact can ask for a decision and get it back, not just be read.
- Approvals, form responses and comments anchored to exact text, each signed with the person's verified email
- The agent reads every answer over MCP, and MCP events or a webhook can wake it when one arrives; fixes are marked addressed in the version that made them
- Owners and agents see who has read which version; readers see what changed since their last visit
Isolated by construction
Tenancy and untrusted code are handled by the platform, not by convention.
- Each workspace is its own Durable Object with its own SQLite database, on its own subdomain and browser origin
- Artifact code runs in a sandboxed, opaque-origin frame with no network access
- Comments live outside the frame and collab writes are stamped server-side, so votes and authorship can't be forged
How artifacts.boo thinks: the principles behind these choices
Prefer to run it yourself?
It's source available: one deploy puts the same thing on your own Cloudflare account, with your own domain and data.
No lock-in. Admins can download everything in their workspace any time, as plain JSON an agent can read (the format), and run it on their own account from the source. Free to run for your own organization under the Functional Source License; offering it to others as a hosted service isn't. Each release becomes Apache 2.0 after two years.
git clone https://github.com/micthiesen/artifacts.boo
cd artifacts.boo && bun install
# set DOMAIN, OPERATOR_EMAILS, OPERATOR_TOKEN in .env
bun run deploy