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) | PostgreSQL | MySQL | |
|---|---|---|---|
| Config | Default | "database": "pg" | "database": "mysql" |
| Managed by | Void | Bring your own database | Bring your own database |
| Best for | Prototypes, read-heavy apps | Complex queries, existing Postgres | Existing MySQL infrastructure |
| Connection | Automatic D1 binding | Hyperdrive | Hyperdrive |
Schema Definition
Define your tables in db/schema.ts using column helpers from void/schema-d1, void/schema-pg, or void/schema-mysql.
// 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'))`),
});// 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(),
});// 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:
// 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:
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
const allUsers = await db.select().from(users);Filter with where
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
// 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
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
await db.delete(users).where(eq(users.id, 1));Column selection
const names = await db.select({ name: users.name, email: users.email }).from(users);
// names: { name: string; email: string }[]Ordering and pagination
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
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:
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:
db/seed.tsdb/seed.mtsdb/seed.jsdb/seed.mjsdb/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.
// 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 databaseschema: the exports from yourdb/schema.tsordb/schema/modules
SQL seeding
If you prefer raw SQL, keep using db/seed.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.
// 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(),
});// 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()),
});// 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():
// 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:
| Function | Purpose |
|---|---|
createInsertSchema | Validates insert data, excluding auto-generated columns and applying defaults |
createSelectSchema | Matches the shape of selected rows |
createUpdateSchema | Like 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:
npm install zod
# or
npm install valibot
# or
npm install arktypeCLI Commands
| Command | Purpose |
|---|---|
void db push | Apply schema directly to local database (no migration files) |
void db connect | Connect or provision PostgreSQL/MySQL and save DATABASE_URL |
void db generate | Generate app and Better Auth SQL migrations for production |
void db migrate | Apply pending migrations locally |
void db status | Show schema drift and pending migrations |
void db reset | Drop the local DB and re-apply all migrations |
void db seed | Reset + run seed file (--file <path>) |
void db execute | Run ad-hoc SQL against local DB (--file <path>) |
void db studio | Open Drizzle Studio for the local database |
void db export | Dump local DB as SQL (--output, --no-data, --no-schema) |
void db set-url | Update 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.
void gen model post title:string body:text published:booleanThis creates:
db/schema/post.ts: a Drizzle table definition withid,createdAt,updatedAt, and your columns- Updates
db/schema.tswithexport * from "./schema/post" routes/api/posts/index.ts:GETfor list andPOSTfor insert with validationroutes/api/posts/[id].ts:GETby id with404handling
See the CLI reference for the full list of generators.