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.

A ChaiBuilder site is one Next.js application that plays two roles at once: it serves your public website, and it hosts the editing tools that produce that website. There is no separate backend, no vendor API in the request path, and no build step between editing and publishing.
If you hold one idea, hold this one: the builder is a client-side editing surface that talks to your own server, and your published pages are rendered by that same server from rows in your own database. Everything below is detail on that sentence.
The product is built in layers, each extending the one below. Knowing which layer you are looking at explains most "can I do X?" questions.
| Layer | What it is | Where it lives |
|---|---|---|
| 1. Editor | A pure React component: blocks in, visual editing, blocks JSON out. It knows nothing about pages, servers, or databases. | chaicore |
| 2. Website builder | The editor plus everything a website needs: pages, SEO fields, publish flow, server wiring, page storage, and page rendering. | chaicore |
| 3. Pro | The website builder plus the Pro extensions - revisions, multilingual, redirects, roles, media management, advanced AI, and more. | chaipro |
Layer 1 is why "embed the builder in any React app" is true - the editor component in
chaicore has no server of its own. Layers 2 and 3 are what you get
when you want a whole website, which is what the starter ships. See
Open Source Core for the boundary in detail.
A deployed site exposes four distinct surfaces. They share a config, a database, and a request context, but they have very different characteristics.
Your Next.js app
│
├── / public site server-rendered, static, cached
├── /admin/editor visual builder client bundle, authenticated
├── /admin/api action endpoint server, authenticated, one route
└── /admin CMS admin Payload, authenticated
All four move together if you relocate /admin - see Admin Route.
The split is strict, and it is what keeps a visual editor safe to expose to non-technical editors.
Runs on the client (the builder bundle)
Runs on the server
The practical consequence: a custom block's props panel is client code, while its data provider is server code. When something needs a secret or a query, it belongs on the server.
Configuration is deliberately split so that plain data stays importable by scripts, CI, and the Payload CLI, while per-request concerns stay in one place.
| File | Job |
|---|---|
next.config.ts |
Next.js integration and the generated import map |
chaibuilder.config.ts |
Server config as plain data: database, plugins, page types, providers, AI models |
chaibuilder.server.ts |
The per-request handle every server file imports |
payload.config.ts |
Payload CMS plus the ChaiBuilder plugin and schema hook |
Full option reference: Configuration.
There is no feature-flag matrix. Registering a plugin turns its feature on, and the plugin's options carry its configuration:
plugins: [
revisionsPlugin({ drafts: true, maxRevisions: 10 }),
redirectsPlugin(),
multilingualPlugin(),
]
Most features have two halves: a server plugin (actions, permissions, data) and a client
plugin (the UI). You register the server half in chaibuilder.config.ts and pass the client
half to the builder component. Keep the two lists mirrored: a server plugin with no client
plugin has no UI, and a client plugin with no server plugin has UI that cannot save.
Because the two lists are explicit, a feature you do not register also does not ship in the editor bundle.
ChaiBuilder keeps its own tables for pages, site settings, assets, and plugin data. Payload keeps its collections. They live in the same database and, in the starter, share a single connection pool.
Your database
├── ChaiBuilder tables pages (draft + published), site settings, assets, plugin data
└── Payload collections blog, legal, media, users, and anything you add
A schema hook merges the ChaiBuilder tables into Payload's schema before migrations are generated, so one migration pipeline covers both. The full set of plugin tables is created whether or not the plugin is registered, which means enabling a feature later needs no migration.
One database can also host many sites. Each site is a row in the apps table with all of its
content scoped to it; a single-site deployment pins itself to one row with
CHAIBUILDER_APP_KEY.
Because the deployment is your own Next.js app, extension points are ordinary code:
| You want to | You write |
|---|---|
| A new block type editors can drop on a page | A registered block: React component plus a props schema |
| Server data for that block | A block data provider in chaibuilder.config.ts |
| A page whose layout is designed once and reused per record | A page type with a data provider |
| A new operation the builder can call | A custom action, permission-checked like the built-ins |
| Extra UI in the builder | A registered panel, slot, or settings field |
| Anything else | A normal Next.js route, Payload collection, or server function |
See Custom Blocks and API.
