Database
Integrate Drizzle ORM with databases in Ploy Start.
Database
Ploy Start provides built-in Drizzle ORM integration for databases, giving you type-safe database queries with automatic schema inference.
Setup
1. Install Drizzle
pnpm add drizzle-orm2. Configure DB Binding
Add a database binding to your ploy.yaml:
kind: worker
build: pnpm build
out: dist
db:
DB: default3. Generate Types
Run the Ploy CLI to generate environment types:
pnpm typesThis creates env.d.ts with your DB binding typed.
4. Add Migrations
Create SQL migrations in migrations/. Ploy runs them in filename order and tracks applied files in ploy_internal_db_migrations inside the same database.
For a single DB binding:
migrations/
└── 20260403171410_init/
├── migration.sql
└── snapshot.jsonOnly .sql files are executed, so sibling files such as snapshot.json are ignored.
For multiple DB bindings, scope migrations by binding name:
migrations/
├── DB/
│ └── 001_create_users.sql
└── ANALYTICS_DB/
└── 20260403171410_init/
└── migration.sqlProduction deploys on the default branch apply pending migrations before upload and log them in the build output. During local development, ploy dev applies project migrations on startup.
To use a different directory, set migrations in ploy.yaml to a path relative to the config file (for example, migrations: ../db/migrations).
Schema Definition
Define your database schema using Drizzle:
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
email: text("email").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
() => new Date(),
),
});
export const posts = sqliteTable("posts", {
id: integer("id").primaryKey({ autoIncrement: true }),
title: text("title").notNull(),
content: text("content"),
authorId: integer("author_id").notNull(),
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
() => new Date(),
),
});Using withDrizzle
The withDrizzle state factory adds a typed db instance to your handler context:
import { ploy, withDrizzle, z } from "@meetploy/start";
import { eq } from "drizzle-orm";
import * as schema from "./schema.js";
import { users } from "./schema.js";
const worker = ploy<PloyEnv>()
.state(withDrizzle("DB", schema))
.get(
"/users",
{
response: z.object({
users: z.array(
z.object({
id: z.number(),
name: z.string(),
email: z.string(),
}),
),
}),
},
async (ctx) => {
// ctx.state.db is typed with your schema
const allUsers = await ctx.state.db.select().from(users);
return { users: allUsers };
},
)
.build();
export default worker;The withDrizzle function takes two arguments: 1. The binding name (must
match your ploy.yaml db key) 2. Your schema object (optional, but
recommended for typed queries)
Query Examples
Select All
const allUsers = await ctx.state.db.select().from(users);Select with Where
import { eq } from "drizzle-orm";
const user = await ctx.state.db
.select()
.from(users)
.where(eq(users.id, 1))
.limit(1);Select Specific Columns
const userNames = await ctx.state.db.select({ name: users.name }).from(users);Insert
await ctx.state.db.insert(users).values({
name: "Alice",
email: "alice@example.com",
});Update
await ctx.state.db
.update(users)
.set({ name: "Alice Smith" })
.where(eq(users.id, 1));Delete
await ctx.state.db.delete(users).where(eq(users.id, 1));Raw DB Access
For operations not supported by Drizzle, access the DB directly via ctx.env:
const result = await ctx.env.DB.exec(`
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
PRAGMA optimize;
`);Prepared Statements
const result = await ctx.env.DB.prepare("SELECT * FROM users WHERE id = ?")
.bind(userId)
.first();Batch Operations
const results = await ctx.env.DB.batch([
ctx.env.DB.prepare("INSERT INTO users (name, email) VALUES (?, ?)").bind(
"Alice",
"alice@example.com",
),
ctx.env.DB.prepare("INSERT INTO users (name, email) VALUES (?, ?)").bind(
"Bob",
"bob@example.com",
),
]);Using withRawDB
For raw DB access as state, use withRawDB:
import { ploy, withRawDB } from "@meetploy/start";
const worker = ploy<PloyEnv>()
.state(withRawDB("DB"))
.get("/users", {...}, async (ctx) => {
const users = await ctx.state.rawDb.query("SELECT * FROM users");
return { users };
})
.build();Raw DB Interface
interface RawDBHelpers {
query<T>(sql: string, ...params: unknown[]): Promise<T[]>;
first<T>(sql: string, ...params: unknown[]): Promise<T | null>;
execute(sql: string, ...params: unknown[]): Promise<void>;
batch(
statements: { sql: string; params?: unknown[] }[],
): Promise<unknown[][]>;
}Full CRUD Example
This example assumes your users table already exists from a migration in migrations/.
import { ploy, withDrizzle, z } from "@meetploy/start";
import { eq } from "drizzle-orm";
import * as schema from "./schema.js";
import { users } from "./schema.js";
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string(),
});
const worker = ploy<PloyEnv>()
.state(withDrizzle("DB", schema))
// List users
.get(
"/users",
{
response: z.object({ users: z.array(userSchema) }),
},
async (ctx) => {
const allUsers = await ctx.state.db.select().from(users);
return { users: allUsers };
},
)
// Get user by ID
.get(
"/users/:id",
{
params: z.object({ id: z.string() }),
response: z.object({ user: userSchema.nullable() }),
},
async (ctx) => {
const [user] = await ctx.state.db
.select()
.from(users)
.where(eq(users.id, parseInt(ctx.params.id)))
.limit(1);
return { user: user ?? null };
},
)
// Create user
.post(
"/users",
{
body: z.object({
name: z.string(),
email: z.string().email(),
}),
response: z.object({ user: userSchema }),
},
async (ctx) => {
const [user] = await ctx.state.db
.insert(users)
.values(ctx.body)
.returning();
return { user };
},
)
// Update user
.put(
"/users/:id",
{
params: z.object({ id: z.string() }),
body: z.object({
name: z.string().optional(),
email: z.string().email().optional(),
}),
response: z.object({ user: userSchema }),
},
async (ctx) => {
const [user] = await ctx.state.db
.update(users)
.set(ctx.body)
.where(eq(users.id, parseInt(ctx.params.id)))
.returning();
return { user };
},
)
// Delete user
.delete(
"/users/:id",
{
params: z.object({ id: z.string() }),
response: z.object({ success: z.boolean() }),
},
async (ctx) => {
await ctx.state.db
.delete(users)
.where(eq(users.id, parseInt(ctx.params.id)));
return { success: true };
},
)
.build();
export default worker;Best Practices
- Use migrations - Keep schema changes in
migrations/instead of request handlers - Index frequently queried columns - Add indexes for better performance
- Validate input - Use Zod schemas to validate data before inserting
- Handle errors - Wrap database operations in try/catch
- Use transactions - Group related operations when needed
How is this guide?
Last updated on