Skip to content

Schema Generation

Rebase uses a schema-as-code pipeline where your TypeScript collection definitions are the single source of truth. The CLI transforms them through a deterministic pipeline:

Collections (TypeScript) → Drizzle Schema → SQL Migrations → PostgreSQL

This page covers every CLI command involved in that pipeline.

Your collection definitions in config/collections/ describe tables, columns, types, relations, and enums. The schema generate command reads these and outputs a Drizzle ORM schema file.

From the generated Drizzle schema, db generate diffs against the current database state and produces timestamped SQL migration files.

The db migrate command applies pending migrations to your PostgreSQL database.

Generate a Drizzle ORM schema file from your collection definitions:

rebase schema generate

What it does:

  • Reads all collections from config/collections/
  • Generates backend/src/schema.generated.ts with Drizzle table definitions, enums, and relations

Options:

Flag Description
--collections, -c Path to collections directory (default: config/collections/)
--output, -o Output path for the generated schema file
--watch, -w Watch for changes and regenerate automatically

Watch mode is useful during development — edit a collection file and the schema regenerates instantly:

rebase schema generate --watch

Reverse-engineer collection definitions from an existing PostgreSQL database:

rebase schema introspect

What it does:

  • Connects to your database (using the connection string from your .env)
  • Inspects all tables, columns, types, and foreign keys
  • Generates collection definition files

Options:

Flag Description
--output, -o Output directory for generated collection files

This is useful when adopting Rebase on an existing database — introspect first, then customize the generated collections.

What the generated files look like

Introspection writes collections against defineCollection, which keeps the property keys literal — so propertiesOrder, listProperties, sort and display.title complete over your own column names, and a key left behind by a renamed column is a compile error rather than a line that quietly does nothing. Which one it imports depends on what your project depends on, and is decided per run:

Your project declares Generated collections use admin block
@rebasepro/cms-types (a project with the panel) defineCollection from @rebasepro/cms-types yes
@rebasepro/common (a --headless project) defineCollection from @rebasepro/common no
neither a PostgresCollectionConfig annotation, with a warning no

A headless project has no admin panel, and the core types declare no admin field at all — so the block is not emitted there. It is presentation (icon, propertiesOrder, multiline); nothing about the schema, the API or your data depends on it.

Push schema changes directly to the database without migration files:

rebase db push

What it does:

  • Reads the generated Drizzle schema
  • Applies changes directly to the database (CREATE, ALTER, DROP)
  • Applies your collections’ RLS policies, and removes policies an earlier push superseded
  • Does not create migration files

The files it generates on the way, all under drizzle/:

File Holds
schema.sql Tables, columns, constraints and indexes — Atlas’s desired state, and the only one it diffs
policies.sql The RLS policies your securityRules compile to
search.sql The full-text search functions and generated columns, for collections with a search block
vector.sql pgvector extensions and ANN indexes
triggers.sql rebase.set_updated_at() and the BEFORE UPDATE triggers behind autoValue: "on_update"

Atlas manages the first and nothing else, so db push and the boot-time schema ensure apply the other four themselves. A migration-only deployment — one that runs db migrate and never db push — has to fold those four into a migration by hand; db generate says so when a change is invisible to Atlas.

Generate SQL migration files from schema changes:

rebase db generate

What it does:

  • Compares the Drizzle schema against the current database state
  • Produces timestamped SQL migration files in the drizzle/ directory
  • Files can be reviewed, edited, and committed to version control

The generated migrations are plain SQL files — you can inspect and modify them before applying.

Run all pending migrations:

rebase db migrate

What it does:

  • Reads the drizzle/ directory for unapplied migrations
  • Applies them in order to the database
  • Tracks which migrations have been applied

Baselining a database Rebase has already booted

Section titled “Baselining a database Rebase has already booted”

Every Rebase boot ensures the schema, and rebase db push applies it directly. So a database that has ever run either one already has the tables and types the first migration would create, and rebase db migrate stops on pq: type "posts_status" already exists (42710).

Nothing is wrong with the migration — the database was provisioned another way. Record where it already is, then migrate normally:

rebase db migrate --baseline 20260906101530
rebase db migrate

The version is the numeric prefix of the migration file that describes what is in the database now. That migration and everything before it are recorded as applied; everything after it runs. On a database nothing has ever booted against, no baseline is needed — migrate straight away.

Database branching for parallel development:

rebase db branch create feature_auth
rebase db branch switch feature_auth
rebase db branch list
rebase db branch switch --off
rebase db branch info feature_auth
rebase db branch delete feature_auth

rebase db branch switch (and switch --off) and rebase db branch prune are new. create, list, info and delete shipped in 0.17.

Each branch is a full copy of the database made with CREATE DATABASE ... TEMPLATE, so it needs a real PostgreSQL server and it costs the same disk as its source.

Detect three-way drift between your collection definitions, the generated Drizzle schema, and the live PostgreSQL database:

rebase doctor

What it checks:

  • Collections ↔ Generated schema — are they in sync?
  • Generated schema ↔ Database — are there unapplied changes?
  • Collections ↔ Database — is there any unexpected drift?

Run doctor whenever something feels out of sync. It pinpoints exactly where the mismatch is.

The database comparison needs DATABASE_URL (or ADMIN_CONNECTION_STRING). Without one, that phase is reported as skipped rather than passing, and the run never closes with “All schemas are in sync” — a check that did not happen is not a clean bill of health.

Generate a typed client SDK from your collection definitions:

rebase generate-sdk

What it does:

  • Reads collections from config/collections/ (supports index.ts barrel exports or individual files)
  • Generates TypeScript types for all entities in generated/sdk/
  • Produces a database.types.ts file for use with createRebaseClient<Database>()

Options:

Flag Description
-c, --collections-dir Path to the collections directory (default: config/collections/)
-o, --output Output directory for the SDK (default: generated/sdk/)
--from <link|url> Read the schema from a running project instead of local source. link uses this checkout’s linked project.
--token Bearer token for the contract endpoint (default: $REBASE_SERVICE_KEY)

--from is what lets a repository that contains no collections — a separate frontend, a second web app, a mobile app — generate a typed client from the project it talks to. REBASE_SERVICE_KEY is only sent to the project this checkout is linked to; pass --token explicitly for any other host.

Usage after generation:

import { createRebaseClient } from "@rebasepro/client";
import { collectionsDictionary, type Database } from "./generated/sdk/database.types";
const client = createRebaseClient<Database>({
baseUrl: import.meta.env.VITE_API_URL,
collections: collectionsDictionary,
});
// Full type safety and autocomplete
const { data } = await client.data.products.find();

Field names in the generated types are the ones the API serves. A field’s wire name is its property key, and the API is camelCase throughout: introspection generates a created_at column as a createdAt property carrying columnName: "created_at", and serves it as row.createdAt. The column itself is untouched. The collection accessor is turned into a property name the same way (my-notesclient.data.myNotes), which is what collectionsDictionary maps back to the slug.

The fast-iteration workflow for development:

# 1. Edit your collection in config/collections/
# 2. Generate the Drizzle schema
rebase schema generate
# 3. Push directly to dev database
rebase db push

The safe, reviewable workflow for production:

# 1. Edit your collection in config/collections/
# 2. Generate the Drizzle schema
rebase schema generate
# 3. Generate SQL migration files
rebase db generate
# 4. Review the generated SQL in drizzle/
# 5. Commit the migration to version control
git add drizzle/
# 6. Apply in production
# A database Rebase has already booted needs a baseline the first time —
# see "Baselining a database Rebase has already booted" above.
rebase db migrate
Symptom Solution
Could not detect an active database plugin Install @rebasepro/server-postgres in backend/package.json
Schema file not updating Check the --collections path points to the right directory
Migration shows unexpected changes Run rebase doctor to identify drift
db push fails on production Use db generate + db migrate instead
db migrate fails with already exists (42710) Boot or db push already provisioned the schema — record it with rebase db migrate --baseline <version>