A page, in 15 lines
Here is a page document that renders a navigation shell with a hero banner, a feature grid, and a footer:page.json
GET this document, edit it locally, and PATCH it back.
Why store pages as documents
Storing the page as a document — rather than as code — is the load-bearing decision in the platform. Four properties follow from it. Portable. A page is a value, so it can move around. You can export a workspace’s pages as JSON, copy them into another workspace, and have a working app — assuming the destination workspace has providers wired for the same capabilities (more on that in Capabilities). Versionable. Every change to a page document creates a new version. Rolling back is a single API call. Diffing two versions is a JSON diff, which the runtime and the Builder both understand. AI-readable. A page is a tree of typed nodes that a language model can read and patch reliably. The Builder MCP exposesget_block_schema so an AI agent can introspect any block type at runtime, compute a patch, and apply it — the same operations the Builder UI performs.
Multi-renderer. The same document drives server-side rendering on first paint, WebAssembly hydration for interactivity, and static HTML export. You write one document; the platform handles the rendering target.
The mental model: blocks as a typed tree
Think of a page as a typed file system — except instead of folders and files, the nodes are blocks. Each block has ablock type (the name), a props object (its configuration), and an optional children array (more blocks nested inside).
The analogy breaks down because folders don’t render themselves and files don’t have schemas. Drop the metaphor once the shape clicks.
Two rules govern the tree:
- Type-checked at write time. Every block has a Zod-validated props schema. The runtime rejects a page document that contains a block whose props don’t match — a
Heromissing aheadlinenever reaches a rendered page. - Bindings are by name, not by reference. A
Formblock binds to a schema table by name ("schema_table": "leads"); aTableblock reads records from a table by name. The binding is resolved at runtime against the workspace’s schema and data.
The 10 block types
Every workspace ships with these. Each one has a typed props schema; the linked reference page is the source of truth for whatprops it accepts.
The page document, in detail
Every page document has four top-level keys. The example at the top of this page showed them in use; here is what each one carries.How a page renders
When a request lands atacme.gavai.app/landing, the runtime walks the tree top-down:
The first paint is server-rendered HTML. The runtime then ships a WebAssembly bundle that hydrates the page for interactivity — Form submissions, Table filters, KanbanBoard drag-drop. The page document is the input to both stages; you do not write two versions.
When to add a block vs. compose existing ones
Most apps fit inside the 10 built-in blocks. A landing page isNavShell → Hero → FeaturesGrid → Cta → Footer. A customer portal is NavShell → Table → DetailPanel. A signup flow is NavShell → Hero → Form.
The 10 cover the common surfaces. If your page wants a shape none of them produce — an embedded video player, a custom chart — that is a sign you need a custom block, which is a separate (and rarer) extension path. Most page changes are prop changes on existing blocks, not new block types.