Databases
Add SQLite databases to your workers with Ploy.
Databases
Ploy supports SQLite databases for workers and applies project migrations automatically. Each project gets its own isolated database that persists across deployments.
Configuration
Add a database binding in your ploy.yaml:
kind: dynamic
build: pnpm build
out: dist
db:
DB: defaultThe key (DB) is the binding name available in your worker's env. The value (default) is the database identifier.
Run ploy types to generate TypeScript types:
import type { Database } from "@meetploy/types";
export interface Env {
DB: Database;
}Basic Example
export default {
async fetch(request, env) {
// Assumes the users table exists from your migrations
await env.DB.prepare("INSERT INTO users (name) VALUES (?)")
.bind("Alice")
.run();
// Query
const { results } = await env.DB.prepare("SELECT * FROM users").all();
return Response.json({ users: results });
},
} satisfies Ploy;Migrations
Create project 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, you can keep migrations at the root:
migrations/
├── 001_create_users.sql
└── 20260403171410_add_posts.sqlNested migration folders are also supported. Any .sql file in the folder is executed, and sibling files like snapshot.json are ignored:
migrations/
└── 20260403171410_init/
├── migration.sql
└── snapshot.jsonIf your project uses multiple DB bindings, scope migrations by binding name:
migrations/
├── DB/
│ └── 20260403171410_init/
│ └── migration.sql
└── ANALYTICS_DB/
└── 001_create_events.sqlProduction deploys on the default branch apply pending migrations before the deployment is uploaded, and the applied files are logged in the build output. During local development, ploy dev applies project migrations on startup.
Query Methods
all() - Get All Rows
const { results } = await env.DB.prepare("SELECT * FROM users").all();first() - Get First Row
const user = await env.DB.prepare("SELECT * FROM users WHERE id = ?")
.bind(1)
.first();run() - Execute Statement
const result = await env.DB.prepare("INSERT INTO users (name) VALUES (?)")
.bind("Bob")
.run();
console.log(result.meta.rows_written); // 1exec() - Run Raw SQL
const result = await env.DB.exec(`
CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, title TEXT);
CREATE INDEX IF NOT EXISTS idx_title ON posts(title);
`);
console.log(result.count); // number of statements executedwithSession() - Session Compatibility
Ploy exposes withSession() so code that expects a session object can keep using prepare() and batch() unchanged.
const session = env.DB.withSession("first-primary");
const user = await session
.prepare("SELECT * FROM users WHERE id = ?")
.bind(1)
.first<User>();TypeScript Support
Add generics for type-safe queries:
interface User {
id: number;
name: string;
}
const users = await env.DB.prepare("SELECT * FROM users").all<User>();
// users.results is User[]
const user = await env.DB.prepare("SELECT * FROM users WHERE id = ?")
.bind(1)
.first<User>();
// user is User | nullAlways use prepared statements with .bind() to prevent SQL injection.
Next Steps
How is this guide?
Last updated on