Skip to main content
vertz db pull connects to an existing database, reads its tables, columns, indexes, and foreign keys, and generates a TypeScript schema file using the d builder. This is the fastest way to bring an existing database into Vertz.

Getting started

1

Configure your database connection

Add a db export to your vertz.config.ts with the connection URL and dialect:
vertz.config.ts
export const db = {
  dialect: 'postgres',
  url: 'postgres://user:pass@localhost:5432/mydb',
  schema: './src/schema.ts',
};
2

Pull the schema

Run vertz db pull to introspect the database and generate a schema file:
vertz db pull --output src/schema.ts
3

Review and customize

The generated schema is a starting point. Review it, add annotations like .readOnly(), .autoUpdate(), and .hidden(), then use it like any hand-written schema.

CLI options

vertz db pull [options]
OptionDescription
-o, --output <path>Output file or directory. If the path ends with /, generates one file per table.
--dry-runPreview the generated code without writing files.
-f, --forceOverwrite existing files.
--url <url>Database URL (overrides vertz.config.ts).
--dialect <dialect>Database dialect: postgres or sqlite.

Output modes

Single file (default)

Generates one schema.ts file with all tables:
vertz db pull --output src/schema.ts
src/schema.ts
import { d } from 'vertz/db';

// ---------- users ----------

export const usersTable = d.table('users', {
  id: d.uuid().primary(),
  email: d.text().unique(),
  name: d.text(),
  createdAt: d.timestamp().default('now'),
});

// ---------- posts ----------

export const postsTable = d.table(
  'posts',
  {
    id: d.uuid().primary(),
    title: d.text(),
    body: d.text().nullable(),
    authorId: d.uuid(),
  },
  {
    indexes: [d.index('authorId', { name: 'idx_posts_author_id' })],
  },
);

export const postsModel = d.model(postsTable, {
  author: d.ref.one(() => usersTable, 'authorId'),
});

Per-table mode

When the output path ends with /, each table gets its own file plus a barrel index.ts:
vertz db pull --output src/schema/
src/schema/
  users.ts
  posts.ts
  index.ts
Each file imports its FK dependencies from sibling files:
src/schema/posts.ts
import { d } from 'vertz/db';
import { usersTable } from './users';

export const postsTable = d.table('posts', {
  id: d.uuid().primary(),
  title: d.text(),
  body: d.text().nullable(),
  authorId: d.uuid(),
});

export const postsModel = d.model(postsTable, {
  author: d.ref.one(() => usersTable, 'authorId'),
});

Zero-config mode

You can skip vertz.config.ts entirely by providing --url and --dialect directly:
vertz db pull --url postgres://localhost:5432/mydb --dialect postgres --dry-run
This is useful for one-off introspection of databases you don’t have a config for.

Type mapping

The code generator maps database types to d builder calls:

Postgres

SQL typeGenerated codeNotes
uuidd.uuid()
textd.text()
character varying(n)d.varchar(n)Falls back to d.text() without length
booleand.boolean()
integerd.integer()
integer + nextval()d.serial()Auto-increment detection
bigintd.bigint()
numeric(p,s)d.decimal(p, s)
reald.real()
double precisiond.doublePrecision()
timestamptzd.timestamp()
timestampd.timestamp()Annotated with // Source: timestamp without time zone
dated.date()
timed.time()
jsonbd.jsonb()
jsond.jsonb()Annotated with // Source: json
text[]d.textArray()
integer[]d.integerArray()
USER-DEFINEDd.enum(name, values)Reads enum values from pg_enum
smallintd.integer()Annotated with // Source: smallint
citextd.text()Case-insensitive text extension
bytead.text()Annotated with // TODO: binary type

SQLite

SQL typeGenerated codeNotes
integerd.integer()
textd.text()
real / floatd.real()
blobd.text()Annotated with // TODO: binary type
Types that don’t map directly get d.text() with a // TODO: unmapped type comment so you can fix them manually.

What gets generated

Columns

  • Names: Snake-case column names are converted to camelCase (created_at becomes createdAt)
  • Constraints: .primary(), .unique(), .nullable() are applied based on the database schema
  • Defaults: Simple defaults (now(), true, false, numeric values, string literals) are preserved. Complex expressions (function calls, casts) are skipped.

Indexes

Indexes are generated with their original name, uniqueness, type, and WHERE clause:
d.index('createdAt', { name: 'idx_users_created_at' }),
d.index(['tenantId', 'email'], { name: 'idx_users_tenant_email', unique: true }),
d.index('status', { name: 'idx_active', where: "status = 'active'" }),

Relations

Foreign keys are detected and generate d.model() with d.ref.one() relations:
export const postsModel = d.model(postsTable, {
  author: d.ref.one(() => usersTable, 'authorId'),
});
  • The relation name is derived from the FK column by stripping Id or Fk suffixes (authorId becomes author)
  • Self-referential FKs work (managerId on employees references employees)
  • Multiple FKs to the same table are disambiguated (sender, usersByReceiverId)
Only d.ref.one() (many-to-one) relations are generated. Inverse d.ref.many() relations are not inferred — add them manually if needed.

Table ordering

Tables are topologically sorted so FK targets are defined before the tables that reference them. Circular references are detected, placed at the end, and annotated:
// Note: circular FK reference with table_b

Composite primary keys

Tables with multiple primary key columns use the primaryKey option instead of .primary() on individual columns:
export const postTagsTable = d.table(
  'post_tags',
  {
    postId: d.uuid(),
    tagId: d.uuid(),
    createdAt: d.timestamp().default('now'),
  },
  {
    primaryKey: ['postId', 'tagId'],
  },
);

What is NOT generated

The code generator produces a database-level schema — the structural representation of what exists in the database. App-level annotations that carry semantic meaning are left for you to add:
AnnotationWhy it’s not generated
.readOnly()Database can’t tell which columns are read-only in your app
.autoUpdate()Database triggers exist but don’t map cleanly to Vertz’s auto-update
.hidden()Field visibility is an app concern
.tenant()Tenant scoping is a framework convention, not a DB concept
.email(), .url()Format validation is application logic
d.ref.many()Inverse relations require knowing which side is “primary”
After pulling, review the generated schema and add these annotations where appropriate.

Preview before writing

Use --dry-run to see what would be generated without writing any files:
vertz db pull --dry-run
The generated code is printed to stdout, so you can pipe it:
vertz db pull --dry-run > schema-preview.ts

Workflow: adopting Vertz on an existing database

1

Pull the schema

bash vertz db pull --output src/schema.ts
2

Review and annotate

Add .readOnly(), .autoUpdate(), .hidden(), .tenant(), and other annotations. Fix any // TODO comments for unmapped types.
3

Baseline the migration history

bash vertz db baseline This marks the current database state as the starting point — no SQL is applied.
4

Start developing

From here, use vertz dev for automatic migrations or vertz db migrate for explicit migration files. See the migrations guide for details.

Schema

Full reference for the d builder — all column types, modifiers, and annotations.

Migrations

How migrations work in development and production.