Type generation
Generate TypeScript types, Zod schemas, a Drizzle schema, Go structs, or Python models from the live database schema - and gate CI on schema drift.
Quickstart
In a linked project directory (capydb link):
capydb generate types # writes database.types.tsEvery language - TypeScript, Zod, Drizzle, Go and Python - is generated server-side against the live schema of your cell, one implementation shared by the CLI (v2.1.0 and later), the API, and the MCP server, so the output is identical no matter which surface asked for it. CLI releases before v2.1.0 rendered Go and Python locally; the server's output is byte-for-byte the same, so upgrading changes no generated file.
Every generator takes the same flags:
--out <path>/-o- output file (defaults to the generator's suggested filename)--print- print to stdout instead of writing a file--preview <id>- generate from a preview database instead of the project database--project <ref>- project id, slug, or name (defaults to the linked project)--watch- keep running and regenerate whenever the schema changes (see Watching for changes)
The dashboard's Schema page shows the same schema and generates the same files - TypeScript, Zod,
Drizzle, Go, and Python in both styles (Python · Pydantic and Python · dataclasses). Go uses the
default package name, db; generate from the CLI or the API to pick another. Its database picker
switches between the project database and any ready preview (each preview on the Previews page links
to its schema).
TypeScript
The default style (capydb) emits per-table Row / Insert / Update interfaces plus a Database aggregate that mirrors them:
export interface UsersRow { ... }
export interface UsersInsert { ... } // generated/defaulted columns optional
export interface UsersUpdate { ... } // everything optional
export type Database = {
public: {
Tables: {
users: { Row: UsersRow; Insert: UsersInsert; Update: UsersUpdate };
};
...
};
};Porting from Supabase
--style supabase emits a supabase-js-compatible Database generic instead, including the Tables<T> / TablesInsert<T> / TablesUpdate<T> / Enums<T> helper types:
capydb generate types --style supabaseIf you imported a Supabase project and your code is full of Tables<"users"> references, this style is a drop-in replacement for supabase gen types typescript - regenerate against your CapyDB cell and the existing type references keep compiling.
Zod
capydb generate zod # writes database.zod.tsEmits a z.object(...) row schema per table plus inferred Row types - useful when you validate data at runtime (API boundaries, form input) rather than only at compile time.
Two ways to Zod
There are two complementary sources of truth for runtime validators:
- Database-first -
capydb generate zodderives validators from the live schema. Right when the database is the source of truth (imported/legacy schemas, SQL-managed migrations, or no ORM at all): the validators track what the database actually enforces. - Schema-first - drizzle v1 ships validator generation in core:
createSelectSchema/createInsertSchemafromdrizzle-orm/zodderive validators from your Drizzle table objects. Right when your Drizzle schema file is the source of truth and migrations flow through drizzle-kit - validators update the moment the schema file does, no regeneration step.
// schema-first: validators straight from your Drizzle tables
import { createInsertSchema, createSelectSchema } from 'drizzle-orm/zod'
import { users } from './db/schema'
const userRow = createSelectSchema(users)
const newUser = createInsertSchema(users)They compose: bootstrap with capydb generate drizzle, then let drizzle-orm/zod derive validators from that schema file going forward.
Drizzle
capydb generate drizzle # writes schema.tsRenders the live schema as a Drizzle ORM schema file - tables, enums, and foreign keys as pgTable / pgEnum declarations. This is the "database-first" entry into Drizzle: point it at an existing cell and start querying with full types. capydb init drizzle goes one step further and scaffolds the config and client around it - see the Drizzle guide.
Go
capydb generate go --package models --out internal/models/models.goOne file with a row struct per table and view (db and json tags carry the column names), a
string type plus constants per enum, and schema/table/column name constants for query building:
type UsersRow struct {
ID string `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Status UserStatus `db:"status" json:"status"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
DeletedAt *time.Time `db:"deleted_at" json:"deleted_at"`
}
const (
UsersTable = "users"
UsersColumnCreatedAt = "created_at"
)Nullable columns are pointers ([]byte, json.RawMessage and slices already carry nil).
numeric and money stay string - a float64 would lose precision - and uuid is a string
so the file needs no dependencies. Types the generator does not know are any. Tables outside
public are prefixed with their schema (BillingInvoicesRow). The tags work with pgx.RowToStructByName,
sqlx, and scany.
Python
capydb generate python --out app/models.py # frozen dataclasses (standard library)
capydb generate python --style pydantic --out app/models.py # pydantic modelsA str enum per enum type and one model per table and view, typed the way psycopg 3 returns each
column: int, float, decimal.Decimal, uuid.UUID, datetime.*, bytes, nested list[...]
for arrays, T | None for nullable columns, and Any for JSON and types the generator does not
know. Each model carries SCHEMA and TABLE class constants. A column whose name is not a valid
Python identifier (class, user id) gets a sanitized attribute name - pydantic models keep the
column name as the field alias, dataclasses note it in a comment.
Watching for changes
capydb generate types --watch --out src/db/database.types.ts--watch keeps the command running and rewrites the file when the schema changes, for any of the
generators. It looks every --watch-interval (default 5s) and backs off to once a minute while
nothing changes, dropping back to the base interval after a change. Reading the schema opens a
connection to the cell, so the watch never reads a paused cell - its schema cannot change until
something wakes it - but each look at an awake cell counts as activity. Stop it when you stop
working. With --output json it prints one JSON line per regeneration.
Generating against a preview branch in CI
The GitHub Action creates a preview database per pull request, and its generate-types command runs this generator against that branch database, so a PR that migrates the schema also carries the matching types:
- name: Create preview database
id: capydb
uses: capydatabase/github-action@v1
with:
api-key: ${{ secrets.CAPYDB_API_KEY }}
project-id: ${{ vars.CAPYDB_PROJECT_ID }}
- name: Run migrations against the preview
run: pnpm db:migrate # DATABASE_URL / DATABASE_URL_UNPOOLED already exported
- name: Generate types from the migrated preview
id: types
uses: capydatabase/github-action@v1
with:
api-key: ${{ secrets.CAPYDB_API_KEY }}
project-id: ${{ vars.CAPYDB_PROJECT_ID }}
command: generate-types
types-path: database.types.ts
github-token: ${{ secrets.GITHUB_TOKEN }} # optional: diff as a PR comment
- name: Fail when the committed types lag the schema
if: steps.types.outputs.types-changed == 'true'
run: exit 1If the generated file differs from what the PR committed, the job fails and the PR comment shows the diff - the types can never silently lag the schema. types-language: zod or drizzle selects the other generators, and types-commit: true commits the regenerated file to the branch instead of failing. The CLI does the same anywhere else: capydb generate types --preview <id> --out database.types.ts, then git diff --exit-code.
Drizzle-kit v1 users should add one more gate before migrating: drizzle-kit check detects migrations that conflict across git branches (two PRs each adding a migration that doesn't commute), and drizzle-kit push --explain previews the exact DDL a push would run without executing it:
- name: Migration hygiene (drizzle-kit v1)
run: pnpm drizzle-kit checkDrift gating with schema dump and diff
capydb schema dump writes the canonical schema document as deterministic JSON - the same structural facts the generators consume (tables, columns, constraints, enums, extensions). Because the output is deterministic, it can be committed and diffed like source:
capydb schema dump --out schema.jsoncapydb schema diff compares the live schema against a committed snapshot or another preview database, and --exit-code makes it behave like git diff --exit-code - differences flip the exit status so CI can gate on drift:
# Has production drifted from the committed snapshot?
capydb schema diff --against schema.json --exit-code
# What does this PR's preview change relative to production?
capydb schema diff --against-preview <preview-id>--against accepts either a bare capydb schema dump file or a raw API response envelope. Combine --preview with --against to check a branch database against the snapshot instead.
Scoped API keys for CI
The schema endpoints accept any API key with projects:read - or a key carrying only the narrow schema:read scope. A schema:read-only key can introspect and generate types but cannot read credentials, run SQL, or touch lifecycle operations, which makes it the right shape for typegen-only CI jobs. Create one in the dashboard under Settings → API keys, project-scoped to the repository's project.
API
Everything above is a thin wrapper over four endpoints (see the Schema tag in the API reference):
GET /v1/projects/{id}/schema
GET /v1/projects/{id}/schema/types?language=typescript|zod|drizzle|go|python
GET /v1/preview-databases/{id}/schema
GET /v1/preview-databases/{id}/schema/types?language=.../schema returns the canonical schema document; /schema/types returns { language, style, filename, content }. The options per language:
language | Option | Values |
|---|---|---|
typescript (default) | style | capydb (default) or supabase |
go | package | Any valid Go package name; default db |
python | style | dataclass (default) or pydantic |
zod, drizzle | - | - |
Options that do not apply to the chosen language are ignored.
curl -s -H "Authorization: Bearer $CAPYDB_API_KEY" \
"https://capydb.dev/api/capydb/v1/projects/$PROJECT_ID/schema/types?language=go&package=models" \
| jq -r '.content' > internal/models/models.goFor agents
The MCP server exposes the same capabilities as get_schema and generate_types. get_schema returns the whole schema in one call - agents should prefer it over introspecting pg_catalog with query_sql - and generate_types returns { filename, content } for the agent to write into the repo, with preview_id for branch databases. For the wider pattern of letting agents change schemas safely, see the agent safety loop.