ChaiBuilder Logo

Data Providers

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.

The three kinds

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.

Where each one lands

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.
  • A block provider does not land in 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.

When each runs, and what is cached

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 No. Memoised within one render only Same
Collection (repeater) No. Memoised within one render only Same
Block No. Awaited per block, per render Same

Read that table as a performance budget. Only global data survives across renders, so a slow query there is amortised. Everything else runs on every render, so a slow one is paid again on every cache miss, on every page that uses it.

Page-type providers used to be cached persistently, keyed per page. They are not any more, and the trade is deliberate: running the provider on every regeneration is what lets it declare per-record cache tags, so publishing one blog post can invalidate exactly the pages that rendered it instead of every page of that type. You pay one query per regeneration to get precise invalidation.

The draft-mode column is why previews are always current: persistent caching is turned off for draft renders, so an editor never previews yesterday's data.

Telling the cache what your data depends on

A provider knows something the platform cannot infer: which records it just read. Return that knowledge as $cacheTags and the page's cache entry is tagged with it, so firing the tag later regenerates exactly the pages that used the data.

dataProvider: async ({ pageProps }) => {
  const post = await findPost(pageProps.pageIdentifier);
  return {
    post,
    $cacheTags: [`posts-${siteId}`, ...(post ? [`posts-${siteId}-${post.id}`] : [])],
  };
};

The same convention works for page-type providers, block providers, and repeater data sources. Four things to know:

  • The key is always stripped. $cacheTags never reaches the block, never lands in pageData, and is never bindable.
  • It only applies to live renders. In the builder and in draft mode nothing is registered, because those paths are not persistently cached.
  • Scope every tag to the site. Include the app or site id, or you will invalidate another tenant's pages.
  • Emit a collection-level tag even when the lookup misses. A page cached before the record existed has no per-record tag to fire; the broad tag is the only thing that can heal it.

Firing the tags is covered in Caching & Revalidation.

Blocks that fetch their own 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.

Failure is quiet by design

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:

  • A collection fetch that throws yields an empty list. The repeater renders nothing and the page returns 200. Nothing in the page announces the failure.
  • A missing binding resolves to an empty string, not an error. A typo in a binding path looks exactly like absent data.
  • A provider returning nothing leaves the page rendering with empty text where content should be.

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.

What providers cannot do

  • Not per-visitor. A provider runs once per render and the result is shared by everyone who gets that cached page. Cookies, geolocation, and logged-in state do not belong here. See Rendering Model.
  • Not client-side. Providers are server functions and hold your credentials. Nothing they return should include a secret, because the resolved values end up in the rendered page.
  • Not a write path. Providers read. Mutations are actions.

© ChaiBuilder. All rights reserved.