Build Apps on Erdo
Erdo pages are small apps that run in a managed runtime with first-class, permission-aware access to your data. A page can query your datasets, read and write per-page state, react to other viewers in realtime, and submit events into pipelines — all under your existing Erdo RBAC, with no servers to run. You write plain HTML/CSS/JS (React is available by default). Erdo hosts it, injects thewindow.erdo client, wires data access to your grants, validates the build, and gives you a private editor URL plus an optional public share link.
The model in one line: pages read directly (
queryDataset, kv.get, channel subscribe) and write through governed, RBAC-checked APIs (kv.set, insertRows, submitEvent). Identity is always the signed-in viewer — attribution and authorization come from your token, never from anything the page sends.Quickstart — deploy from Claude Code in under 5 minutes
Pages deploy through the Erdo MCP server, so any MCP client (Claude Code, Claude Desktop, Cursor) can ship one. Connect the server first (see MCP Quick Start), then ask your agent to deploy a page — it callserdo_deploy_page for you.
Ask the agent to deploy
“Deploy a page titled Sales Dashboard that charts my acme.sales dataset by month.”
The agent writes the HTML/JS and calls erdo_deploy_page with dataset_slugs: ["acme.sales"]. You get back an editor URL, validation results, and (if you asked for public: true) a share link.The deploy tools
These are MCP tools (and matching REST endpoints) — the external front door to the same artifact service, validation, and renderer that Erdo’s own agents use.| Tool | What it does |
|---|---|
erdo_deploy_page | Deploy a new HTML page/app. Returns its ID, editor URL, optional public URL, and structured validation results. |
erdo_update_page | Update a deployed page. Fields are merged — send only js to fix a script without resending html/css. |
erdo_validate_page | Dry-run validation (HTML structure, JS/JSX syntax + runtime smoke checks, window.erdo usage, dataset-slug references) without deploying. |
erdo_list_artifacts / erdo_get_artifact | Find and inspect pages you’ve already deployed. |
erdo_deploy_page inputs
| Field | Type | Notes |
|---|---|---|
title | string | Page title shown in Erdo. |
html | string | Full document or a fragment. With the default runtime, include a root element (e.g. <div id="root"></div>) and put React code in js. |
css | string? | Optional stylesheet. |
js | string? | Optional JavaScript/JSX. window.erdo is available; with react-tailwind, React 18 + Tailwind + Erdo UI components are loaded. |
runtime | string? | react-tailwind (default) or none. |
dataset_slugs | string[]? | Datasets the page reads via window.erdo.queryDataset. The page is granted read access to each — you must be able to view them. Unknown slugs are a hard error, not a silent drop. |
writable_dataset_slugs | string[]? | Datasets the page may append to via window.erdo.insertRows. The page is granted write (edit) access to each — you must hold edit yourself. List a dataset in both if the page reads and writes it. |
kv_slugs | string[]? | Named KV stores (collections) the page reads via erdo.kv.get/list with { kv: "slug" }. Granted read access on each. The page’s own private KV store needs no grant. |
writable_kv_slugs | string[]? | Named KV stores the page may write via erdo.kv.set/delete. Granted edit access on each — you must hold edit yourself. |
public | bool? | true makes the page publicly viewable (view-only) at its share URL. Default false: only you and people the page’s thread is shared with can view it. |
Write grants are opt-in.
erdo.insertRows and writes to a named KV store only work when the page was deployed with the matching writable_dataset_slugs / writable_kv_slugs; otherwise they return a permission error. Reads (dataset_slugs), the page’s own private KV store, and submitEvent (pipelines) need no write grant. erdo_update_page takes the same fields to add or replace grants later (send [] to clear one).Runtimes.
react-tailwind (default) preloads React 18, Tailwind, and the Erdo UI SDK (DatasetChart, DatasetTable, …) so your js can render components against query results immediately. none ships your HTML/CSS/JS with no runtime scripts injected — use it for fully hand-rolled pages.The window.erdo client
Every page gets window.erdo, a Promise-based client that talks to the Erdo platform over a sandboxed bridge. Everything it does runs as you (or, on a public page, as the anonymous viewer) under your RBAC.
Reading data
Every dataset is queried as the tabledata — the slug in the call selects which dataset, and the SQL addresses its rows as FROM data, regardless of backend. The dialect follows the storage: file datasets (CSV/Excel) use DuckDB SQL, synced integration datasets use ClickHouse SQL. Both support WHERE / GROUP BY / ORDER BY / CAST / aggregations.
Writing data
Two governed write paths — pick by who’s writing:Per-page state (KV)
erdo.kv (aliased as erdo.collections for already-published pages) is per-page key/value state with two partitions:
shared— one value for the whole page, visible to every viewer. Collaborative state (a counter, a guestbook, a board).viewer— private to the signed-in viewer (their draft, their preferences). Requires a signed-in user.
Realtime channels
erdo.channel(topic) is pub/sub between everyone viewing the page — the basis for multiplayer.
kv.change topic, so you can make state reactive without wiring your own messages:
Live datasets, filters, and theme
KV vs. datasets — which store?
Both store structured values. The decision rule is about who reads it:The rule
If anything other than this page reads the value, it’s a dataset. If only the page reads it, it’s KV.
- Dataset — leads from a form, events, anything an agent will analyze, anything another page or report consumes, anything that needs schema/queries/joins. Write with
insertRowsor adataset.writepipeline. - KV (collection) — page-private memory: UI state, a draft, a shared scratchpad, a small leaderboard that only this page renders. Capped and key/value only.
A reactive multiplayer example
A shared counter every viewer sees update live, built from two primitives — shared KV + thekv.change channel:
Auth & sharing
- Private by default. A new page is visible only to you and anyone the page’s thread is shared with. Set
public: true(on deploy or update) for a view-only public link at/p/:id. - Identity is the signed-in viewer.
window.erdocalls run as the authenticated user; the page can never impersonate someone — identity comes from your token, server-side. - Reads are viewer-authorized. A page can only read datasets you granted it (
dataset_slugs), and only viewers who can see the page can read its data and shared state. On a public page, anonymous visitors get view/read access to public content and shared state. - Writes are RBAC-checked. Writing the page’s own private KV store requires VIEW on the page plus a signed-in identity. Writing a named (cross-page) KV store or appending to a dataset additionally requires an explicit EDIT grant the page holds — declared at deploy via
writable_kv_slugs/writable_dataset_slugs. Org membership alone is not enough. - One permission model. Pages reuse Erdo’s standard RBAC (
view/edit) — no separate app-level permission system.
Limits
| Limit | Value |
|---|---|
| Collection value size | 64 KB per item |
| Collection keys | 1,000 per partition |
| Event body size | 1 MB per submission |
| Viewer write rate (events) | 300 / minute sustained, burst 60 |
| Page content | HTML + CSS + JS (single page; multi-file bundles are not supported in v1) |
Pages run today’s artifact shape (HTML/CSS/JS) in the Erdo page runtime — they are not arbitrary built bundles (Vite/webpack output). The runtime injects React/Tailwind/the bridge, validates the result, and wires data access. This keeps every page governed by the same validation and RBAC.
Next steps
MCP Server
Connect your MCP client and see the full tool reference, including the page deploy tools.
REST API
Deploy and manage pages over HTTP for CI and scripts.

