Skip to content
brought to you byVoidZero

Database ​

Define your tables in TypeScript, then query them with db from void/db. Void includes Drizzle ORM and configures the connection, so you can use the same schema and query workflow with D1, PostgreSQL, or MySQL.

  • 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 ​

Void supports three database backends. All use the same Drizzle-based workflow for schema definition, querying, and migrations.

void init can start you with D1, PostgreSQL, MySQL, or no database yet. D1 stays implicit; PostgreSQL writes "database": "pg"; MySQL writes "database": "mysql".

D1 (SQLite)PostgreSQLMySQL
ConfigDefault"database": "pg""database": "mysql"
Managed byVoidBring your own databaseBring your own database
Best forPrototypes, read-heavy appsComplex queries, existing PostgresExisting MySQL infrastructure
ConnectionAutomatic D1 bindingHyperdriveHyperdrive

Schema Definition ​

Define your tables in db/schema.ts using column helpers from void/schema-d1, void/schema-pg, or void/schema-mysql.

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

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 your schema across multiple files under db/schema/ and re-export 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 ​

Void re-exports all common Drizzle query operators from void/db so you don't need to install drizzle-orm separately. Import db from void/db and your tables from @schema:

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

What is @schema?

@schema is a Vite path alias that the Void plugin configures automatically. It points to your db/schema.ts file, or db/schema/ if you split tables across files. You can just use it to import table definitions.

List rows ​

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

Filter with where ​

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

// 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 { db, desc } from 'void/db';
import { users } from '@schema';

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

Joins ​

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

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 your schema defines relations, you can use Drizzle's relational query API:

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

The user schema is automatically loaded into the db instance via a Vite plugin, so relational queries work out of the box.

⚠️ Nuxt and Analog limitations

Nuxt and Analog use Nitro, which bundles server routes outside of Vite's plugin pipeline. The schema cannot be injected into the db instance, so db.query.* relational queries are not available in Nuxt and Analog. Use the standard query builder API (db.select().from(table)) instead.

Seeding ​

Use void db seed to reset your local database, re-apply migrations, and then run a seed file.

Void resolves default seed files in this order:

  1. db/seed.ts
  2. db/seed.mts
  3. db/seed.js
  4. db/seed.mjs
  5. db/seed.sql

If more than one default seed file exists, pass --file <path> explicitly.

Programmatic seeding ​

Use db/seed.ts when you want to generate seed data in code. Export a default function 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" or "postgresql"
  • db: a Drizzle instance for the local database
  • schema: the exports from your db/schema.ts or db/schema/ modules

SQL seeding ​

If you prefer raw SQL, keep using db/seed.sql:

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

Schema-Derived Validators ​

You can derive request validators from your Drizzle tables with void/drizzle-zod, void/drizzle-valibot, or void/drizzle-arktype. These adapters use Void's bundled Drizzle version, keeping the validators compatible with your table types.

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

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
  email: text('email').notNull(),
  createdAt: text('created_at')
    .notNull()
    .default(sql`(datetime('now'))`),
});

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

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
  email: text('email').notNull(),
  createdAt: text('created_at')
    .notNull()
    .default(sql`(datetime('now'))`),
});

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

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
  email: text('email').notNull(),
  createdAt: text('created_at')
    .notNull()
    .default(sql`(datetime('now'))`),
});

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-increments become optional, while 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.

Use the derived schema in your route handlers with withValidator():

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

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)

Void already bundles the Drizzle adapters. Install the validator library itself if you use its direct APIs in your schema file:

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

CLI Commands ​

CommandPurpose
void db pushApply schema directly to local database (no migration files)
void db connectConnect or provision PostgreSQL/MySQL and save DATABASE_URL
void db generateGenerate app and Better Auth SQL migrations for production
void db migrateApply pending migrations locally
void db statusShow schema drift and pending migrations
void db resetDrop the local DB and re-apply all migrations
void db seedReset + run seed file (--file <path>)
void db executeRun ad-hoc SQL against local DB (--file <path>)
void db studioOpen Drizzle Studio for the local database
void db exportDump local DB as SQL (--output, --no-data, --no-schema)
void db set-urlUpdate the PostgreSQL or MySQL connection string for deployment

See the CLI reference for details.

Scaffolding ​

The void gen model command generates dialect-appropriate sqliteTable, pgTable, or mysqlTable definitions.

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

This creates:

  1. db/schema/post.ts: a Drizzle table definition with id, createdAt, updatedAt, and your columns
  2. Updates db/schema.ts with export * from "./schema/post"
  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.