The view contract
The sandbox constraints every view must respect.
A view runs inside a sandboxed iframe with host-owned CSP. This is not a
detail — it shapes everything you build. The host CSP/sandbox is the runtime
security boundary; the @rectsh/rect/vite plugin only catches unsupported URL
forms at build time.
Self-contained output
A view ships as a bundle: an index.html and its assets, which the host
serves together. Prefer relative references — ./assets/app.js, a data:
URI, a blob:, or a #anchor. The build fails unsupported URL forms:
http:// and protocol-relative //; runtime https:// loads are governed by
the host CSP/sandbox.
Practically: import assets when you can so the bundler emits them into the
bundle. (Setting base: './' keeps Vite asset URLs relative; the
@rectsh/rect/vite plugin does this for you.)
Prefer a single file? rect({ singleFile: true }) inlines everything into one
index.html instead — handy for pasting or hand-editing. Either output obeys
the same unsupported-URL-form lint.
State goes through the host
Reads and writes go to the parent host over postMessage, and the host does the
persistence. Direct runtime https:// requests are a host policy decision, not
a Vite plugin rule.
For view state, use the store (connect()). It speaks the host protocol for
you.
No same-origin storage
localStorage, sessionStorage, and cookies throw in an opaque-origin document.
Keep all state in the view model — that's what gets persisted and synced anyway.
Size cap
The compiled bundle must be ≤ 20 MB. React + your app is ~200 KB, so there's plenty of room, but large embedded assets add up. Prefer SVG and compressed images; reach for a lighter framework (Preact, Solid) if you need a tiny bundle.
The self-describing spec
Every view embeds an inert spec block so the platform (and agents) know its name and an example view model:
<script type="application/rect-view+json">
{
"name": "Note",
"description": "Use when an agent and a person need to draft a shared note.",
"example": { "title": "Untitled", "body": "" }
}
</script>You don't write this by hand — declare it in rect.view.json (or the plugin's
spec option) and the build injects and validates it. name and example are
required; description, agentInstructions, and stateSchema are optional.
SDK projects normally author agentInstructions in rect.agent.md; the Vite
plugin reads that Markdown automatically. Use description only to tell an
agent when to choose the Rect, and use the Markdown file for instructions on
creating and updating it.
Describe state with stateSchema
stateSchema is optional JSON Schema-shaped metadata that describes the
complete view model to agents, template search, and other tooling. Add it when
field names alone do not make the state shape and allowed values obvious.
{
"name": "Review queue",
"description": "Use when a person needs to review a prepared queue.",
"stateSchema": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Human-readable queue title"
},
"status": {
"type": "string",
"enum": ["draft", "ready", "completed"],
"description": "Current queue lifecycle state"
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"summary": { "type": "string" }
},
"required": ["id", "summary"],
"additionalProperties": false
}
}
},
"required": ["title", "status", "items"],
"additionalProperties": false
},
"example": {
"title": "Launch review",
"status": "draft",
"items": []
}
}Keep four things aligned: stateSchema, example, the TypeScript state type,
and the fields the UI and actions actually read or write. Prefer an object root,
declare properties and required, use enum for lifecycle values, and add
short field descriptions where meaning or format is not self-evident.
stateSchema is currently descriptive metadata, not runtime validation: the
host does not reject rect_issue, merge patches, or UI writes merely because
they do not conform to it. Put operating instructions in rect.agent.md, and
keep action argument validation in each action's separate inputSchema.