Skip to content
Built with

Database ​

Build & Deploy

Define your tables in TypeScript, then query them with db from void/db. The SDK includes Drizzle ORM and sets up the connection, so the same schema and queries work with D1, PostgreSQL or MySQL.

On this pageWrite a schema · Create and apply migrations · Query and seed your data

  • db/
    • schema.tsTable definitions (source of truth)
    • schema/Split schema files (optional)
      • users.ts
      • posts.ts
    • migrations/Generated SQL migration files
      • 20260410161500_create_users.sql
      • 20260410161501_add_posts.sql
    • seed.tsProgrammatic seed script (optional)
    • seed.sqlRaw SQL seed file (optional alternative)

Choosing a dialect ​

This step picks a dialect. Choose during void init, or set database in void.config.ts.

D1 (SQLite)PostgreSQLMySQL
ConfigDefault"database": "pg""database": "mysql"
Managed byThe SDKBring your own databaseBring your own database
ConnectionAutomatic D1 bindingHyperdriveHyperdrive

An SDK platform keeps Hyperdrive query caching off, so reads see completed writes. An existing connection is updated on the project's next deploy.

Schema definition ​

This step defines tables in db/schema.ts with column helpers from void/schema-d1, void/schema-pg, or void/schema-mysql.

ts
import { sql } from 'void/db';
// db/schema.ts
import { sqliteTable, text, integer } from 'void/schema-d1';

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  role: text('role').notNull().default('user'),
  createdAt: text('created_at')
    .notNull()
    .default(sql`(datetime('now'))`),
});
ts
// db/schema.ts
import { pgTable, serial, text, timestamp } from 'void/schema-pg';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  role: text('role').notNull().default('user'),
  createdAt: timestamp('created_at').notNull().defaultNow(),
});
ts
// db/schema.ts
import { int, mysqlTable, text, timestamp } from 'void/schema-mysql';

export const users = mysqlTable('users', {
  id: int('id').autoincrement().primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull(),
  createdAt: timestamp('created_at').notNull().defaultNow(),
});

The schema helpers are included in void. You can write tables yourself or generate a starting point with void gen model.

You can also split the schema into files under db/schema/ and re-export them from a barrel file:

ts
// db/schema/users.ts
export const users = ...;

// db/schema/posts.ts
export const posts = ...;

// db/schema.ts (barrel)
export * from "./schema/users";
export * from "./schema/posts";

Querying ​

The SDK re-exports common Drizzle query operators from void/db. You do not need drizzle-orm. This step imports db from void/db and tables from @schema.

ts
import { users } from '@schema';
import { db } from 'void/db';

What is @schema?

@schema is a Vite path alias that the SDK plugin configures automatically. It points to db/schema.ts, or db/schema/ if you split tables. Use it to import table definitions.

List rows ​

ts
const allUsers = await db.select().from(users);

Filter with where ​

ts
import { users } from '@schema';
import { db, eq, and, or } from 'void/db';

// Single condition
const user = await db.select().from(users).where(eq(users.id, 1));

// Multiple conditions
const admins = await db
  .select()
  .from(users)
  .where(and(eq(users.role, 'admin'), eq(users.name, 'Alice')));

// OR conditions
const result = await db
  .select()
  .from(users)
  .where(or(eq(users.role, 'admin'), eq(users.role, 'editor')));

Insert ​

ts
// Single row
await db.insert(users).values({
  name: 'Alice',
  email: 'alice@example.com',
});

// Insert and return the created row
const [created] = await db
  .insert(users)
  .values({ name: 'Bob', email: 'bob@example.com' })
  .returning();

// Multiple rows
await db.insert(users).values([
  { name: 'Alice', email: 'alice@example.com' },
  { name: 'Bob', email: 'bob@example.com' },
]);

Update ​

ts
await db.update(users).set({ role: 'admin' }).where(eq(users.id, 1));

// Update and return the modified row
const [updated] = await db.update(users).set({ role: 'admin' }).where(eq(users.id, 1)).returning();

Delete ​

ts
await db.delete(users).where(eq(users.id, 1));

Column selection ​

ts
const names = await db.select({ name: users.name, email: users.email }).from(users);
// names: { name: string; email: string }[]

Ordering and pagination ​

ts
import { users } from '@schema';
import { db, desc } from 'void/db';

const page = await db.select().from(users).orderBy(desc(users.createdAt)).limit(10).offset(20);

Joins ​

ts
import { users, posts } from '@schema';
import { db, eq } from 'void/db';

const results = await db
  .select({
    id: posts.id,
    title: posts.title,
    author: users.name,
  })
  .from(posts)
  .innerJoin(users, eq(posts.userId, users.id));

Relational queries ​

If the schema defines relations, you can use Drizzle's relational query API:

ts
const usersWithPosts = await db.query.users.findMany({
  with: { posts: true },
});

Seeding ​

void db seed resets the local database, reapplies migrations, then runs a seed file.

Use db/seed.ts for a programmatic seed or db/seed.sql for SQL. JavaScript and .mts / .mjs files also work. With several seed files, pick one with --file <path>.

Programmatic seeding ​

This step generates seed data in code with db/seed.ts. Export a default or a named seed function.

ts
// db/seed.ts
import { defineSeed } from 'void/seed';

export default defineSeed<typeof import('./schema')>(async ({ db, schema }) => {
  const rows = Array.from({ length: 100 }, (_, i) => ({
    text: `Seed message ${i + 1}`,
  }));

  await db.insert(schema.messages).values(rows);
});

The seed context includes:

  • dialect: "sqlite", "postgresql", or "mysql"
  • db: a Drizzle instance for the local database
  • schema: the exports from your db/schema.ts or db/schema/ modules

SQL seeding ​

For raw SQL, use db/seed.sql:

sql
INSERT INTO messages (text) VALUES ('Hello from SQL');

Schema-derived validators ​

Derive request validators from your tables with void/drizzle-zod, void/drizzle-valibot, or void/drizzle-arktype. This step adds the validator after the table definition.

ts
// db/schema.ts
import { createInsertSchema } from 'void/drizzle-zod';

export const insertUserSchema = createInsertSchema(users, {
  name: (schema) => schema.min(1),
  email: (schema) => schema.email(),
});
ts
import { pipe, minLength, email } from 'valibot';
// db/schema.ts
import { createInsertSchema } from 'void/drizzle-valibot';

export const insertUserSchema = createInsertSchema(users, {
  name: (schema) => pipe(schema, minLength(1)),
  email: (schema) => pipe(schema, email()),
});
ts
import { type } from 'arktype';
// db/schema.ts
import { createInsertSchema } from 'void/drizzle-arktype';

export const insertUserSchema = createInsertSchema(users, {
  name: type('string > 0'),
  email: type('string.email'),
});

createInsertSchema generates a validator that matches $inferInsert. Columns with defaults or auto-increment become optional. NOT NULL columns stay required. The optional second argument lets you refine individual columns, either with a callback that receives the generated schema or by passing a type directly to override a field.

This step uses the derived schema in a route handler with withValidator().

ts
import { users, insertUserSchema } from '@schema';
// routes/api/users/index.ts
import { defineHandler } from 'void';
import { db } from 'void/db';

export const POST = defineHandler.withValidator({
  body: insertUserSchema,
})(async (c, { body }) => {
  const [created] = await db.insert(users).values(body).returning();
  return created;
});

Three functions are available:

FunctionPurpose
createInsertSchemaValidates insert data, excluding auto-generated columns and applying defaults
createSelectSchemaMatches the shape of selected rows
createUpdateSchemaLike insert but all fields are optional (partial update)

The SDK bundles the Drizzle adapters. This step installs the validator library, needed only if your schema uses its APIs directly.

bash
npm install zod
# or
npm install valibot
# or
npm install arktype

Example output: npm reports the package added.

CLI commands ​

CommandPurpose
void db pushApply schema changes locally without migration files
void db generateGenerate SQL migrations
void db migrateApply pending migrations locally
void db statusCheck schema drift and pending migrations
void db seedReset the local database and run a seed file

See the CLI reference for connection setup, SQL execution, Studio, exports, and other commands.

Hand-written migrations ​

Deploy compares your Drizzle schema with the snapshots in db/migrations/meta/. With SQL migrations that lack this metadata, void db generate would create a duplicate migration.

Follow these steps to keep an existing migration history:

  1. Make your Drizzle schema match the tables your SQL migrations create.
  2. In a scratch copy of the project, remove db/migrations/ and run void db generate.
  3. Copy the generated meta/ folder into your original migration directory. In _journal.json, replace entries with an entry for each existing SQL file in order. Use consecutive idx values starting at 0, the file name without .sql as tag, increasing timestamps as when, and the generated entry's version. Set breakpoints to whether the file uses --> statement-breakpoint lines.
  4. Rename the generated snapshot to <prefix>_snapshot.json, where <prefix> is the part of the last migration tag before its first _. For 001_create_users.sql, use 001_snapshot.json.

Review and commit the metadata, then run void deploy again. If no database has applied the SQL yet, replace it with migrations from void db generate instead.

For D1 migrations containing triggers, and MySQL migrations containing triggers, procedures, functions or events, add a --> statement-breakpoint line after each complete SQL statement, including the trigger's closing END;. Keep the trigger body together.

Scaffolding ​

This step scaffolds a model. void gen model writes sqliteTable, pgTable or mysqlTable definitions for your dialect.

bash
void gen model posts title:string body:text published:boolean

Example output: The files listed below.

This creates:

  1. db/schema/posts.ts: a Drizzle table definition with id, createdAt, updatedAt, and your columns
  2. Updates db/schema.ts with export * from "./schema/posts"
  3. routes/api/posts/index.ts: GET for list and POST for insert with validation
  4. routes/api/posts/[id].ts: GET by id with 404 handling

See the CLI reference for the full list of generators.

Next steps ​

implemented.stream AI