The Visual Website Builder
You Actually Own. React + Next.js, low-code, self-hosted. Your data, your infrastructure, every feature included.
© ChaiBuilder. All rights reserved.

Blocks do not fetch data. Providers fetch data, the render merges what they returned into one
object, and blocks read from that object through {{ }} bindings.
Separating the two is what lets a non-technical editor bind a heading to a blog post title without knowing where blog posts come from - and lets you change where they come from without touching a single page.
Every provider is a server function. It receives the request's language, whether this is a draft render, whether it is running inside the builder, and the page's details. It returns a plain object.
| Provider | Scope | Runs | Answers |
|---|---|---|---|
| Global | The whole site | Once per render | "What is true of this site?" Site name, contact details, socials, tracking, header and footer HTML |
| Page type | One page | Once per render | "Which record is this page?" The blog post, the legal document, the job listing |
| Block | One block instance | Once per block, per render | "What does this block need?" A pricing table's plans, a sidebar's tree, a live stat |
A fourth thing behaves like a provider without being one: a collection, which a Repeater
block uses to fetch a list of records. Collections declare their own fetch function, and the
render resolves one per repeater block that points at them.
This is the part worth memorising, because it is what determines what you type in a binding.
pageData
├── <page-type keys> ← page-type provider output, spread at the top level
├── global.* ← global provider output, namespaced
└── <collection results> ← attached to the repeater block that asked for them
So a page-type provider returning { blog: {...} } gives you {{ blog.title }}, while the global
provider's { companyName } gives you {{ global.companyName }}. Inside a repeater, bindings
resolve against the current item.
Two consequences:
global is a reserved key at the top level. A page-type provider that returns its own
global key will be overwritten by the global provider's output.pageData at all. Its result is merged into that block's
props, and it is merged last - so a key the provider returns overrides what the editor
configured in the panel. That is occasionally what you want and frequently a surprise; name
provider keys so they cannot collide with editable props.All providers run during a page render - which, because pages are static, means once per cache miss rather than once per visitor. But they are not cached the same way.
| Provider | Cached in production | In draft mode |
|---|---|---|
| Global | Yes, with cache tags, shared across all visitors | Downgraded to per-request memory |
| Page type | Yes, with cache tags keyed per page | Downgraded to per-request memory |
| Collection (repeater) | No. Memoised within one render only | Same |
| Block | No. Awaited per block, per render | Same |
Read that table as a performance budget. Global and page-type data survive across renders, so a slow query there is amortised. A collection fetch or a block provider runs on every render, so a slow one is paid again on every cache miss, on every page that uses it.
The draft-mode row is why previews are always current: persistent caching is turned off for draft renders, so an editor never previews yesterday's data.
A block declaring a provider gets one more thing: the render suspends on it, showing the block's fallback until the data arrives. Adjacent blocks are not blocked, so one slow block does not hold up the page.
Reach for a block provider when the data belongs to the block wherever it is placed - a pricing table, a docs sidebar, a "latest release" badge. Reach for a page-type provider when the data belongs to the page, and a global provider when it belongs to the site. Putting site-wide data in a block provider means fetching it once per block instead of once per render.
Providers sit in the render path of a cached public page, so the platform prefers a slightly empty page over a broken one. That is a reasonable default and a poor debugging experience, so know the behaviours:
So when a section is mysteriously blank, suspect the provider before the block. Turn on server-side debug logging to see the queries and cache decisions for a render - see Configuration.
{{ }} syntax that consumes all of this.