Database Setup
All of your site's content - pages, CMS collections, users, settings - lives in your own database. ChaiBuilder ships its own set of tables alongside Payload's, and both are managed through one migration pipeline, so there is a single database and a single upgrade path.
Choosing a database
| Option | Best for | Connection |
|---|---|---|
| SQLite (local file) | Local development, quick starts | file:./local.db |
| libSQL / Turso | Small production sites, serverless-friendly SQLite | libsql://… URL + auth token |
| Postgres | Production, teams, multi-instance deployments | postgresql://… connection string |
The scaffolder (npx chaibuilder-app create) asks which one you want and writes the
connection into your environment, runs migrations, and seeds the database - so for a new
project there is nothing manual to do. The notes below are for changing databases later or
tuning production.
A local SQLite file is per-instance. On serverless platforms and multi-container deployments, each instance would get its own copy. Use a remote database (Turso or Postgres) for anything beyond a single long-running server.
Connection string
The database connection is a single environment variable:
# Postgres
DATABASE_URL=postgresql://user:password@host:5432/dbname
# libSQL / Turso
DATABASE_URL=libsql://your-db.turso.io
Point this at your production database in your hosting platform's environment settings. The same variable is read by Payload and by ChaiBuilder's own tables - they share one connection.
Postgres pooling
ChaiBuilder opens one shared connection pool for the whole app (Payload and the builder share it, so you never pay for two pools). Pool sizing adapts to your provider automatically:
- Direct Postgres - up to 10 connections per instance.
- PgBouncer / transaction poolers - detected via port
6543or a?pgbouncer=truequery param; the pool shrinks to 5 connections and shortens idle timeouts. - Supabase - pooler detection plus tighter idle timeouts to play well with Supavisor.
On serverless platforms (many short-lived instances), always connect through your provider's pooler endpoint (Neon pooled connection string, Supabase transaction pooler on port 6543) rather than the direct database port, or you will exhaust connections under load.
Postgres schema isolation
To run ChaiBuilder inside a shared database, set an optional schema name and all tables are
created there instead of public:
CHAIBUILDER_POSTGRES_SCHEMA=chaibuilder
Unlimited sites, one database
A single database can host any number of sites. Each site is a row in ChaiBuilder's
apps table, and every page, user membership, and asset is scoped to its site - so adding
a site is a data operation, not a new database or a new deployment. Which site a request
belongs to is resolved from the hostname, or pinned with CHAIBUILDER_APP_KEY for
single-site deployments (see
Environment & License).
This is what makes agency and portfolio setups cheap to run: one deployment, one database, one set of migrations - unlimited sites.
Migrations
The starter uses Payload's migration workflow, and ChaiBuilder's tables are merged into the same schema snapshot - so one command manages everything:
# create a migration after changing collections or upgrading the SDK
pnpm payload migrate:create
# apply pending migrations
pnpm payload migrate
In production, the starter is configured with prodMigrations, which means pending
migrations run automatically when the app boots - a deploy with new migrations applies
them on startup. You can still run pnpm payload migrate explicitly as a release step if
you prefer migrations to happen before traffic switches over.
When you upgrade the ChaiBuilder SDK, the flow is the same as any Payload schema change: bump the package, run
pnpm payload migrate:create, review the generated migration, and deploy. See Releases and Upgrading.
Seeding
A fresh database gets its initial content (admin user, starter pages) from the seed script:
pnpm seed
The scaffolder runs this for you on project creation; you only need it when pointing the app at a brand-new empty database.
Moving from SQLite to a remote database
Starting on a local SQLite file does not lock you in:
- To Turso - import the local file into a Turso database, then switch
DATABASE_URLto thelibsql://URL and set the auth token. - To Postgres - migrate the schema onto Postgres (run migrations against the new database), then move content. Plan this like any database migration; do it before the site accumulates significant content if you can.
Backups
The database is yours, so backups are yours too. Use your provider's point-in-time recovery
(Neon, Supabase, RDS all offer it) or scheduled dumps (pg_dump) for self-hosted Postgres.
For SQLite, snapshot the file while the app is stopped, or use Turso's built-in backups.
Related
- Environment & License - where
DATABASE_URLfits in the full env. - Requirements - supported database versions.
- Hosting Overview - database choice by hosting target.

