Ploy
Ploy
Features

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:

ploy.yaml
kind: dynamic
build: pnpm build
out: dist
db:
  DB: default

The 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:

env.d.ts
import type { Database } from "@meetploy/types";

export interface Env {
	DB: Database;
}

Basic Example

src/index.ts
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/
migrations/
├── 001_create_users.sql
└── 20260403171410_add_posts.sql

Nested migration folders are also supported. Any .sql file in the folder is executed, and sibling files like snapshot.json are ignored:

migrations/
migrations/
└── 20260403171410_init/
    ├── migration.sql
    └── snapshot.json

If your project uses multiple DB bindings, scope migrations by binding name:

migrations/
migrations/
├── DB/
│   └── 20260403171410_init/
│       └── migration.sql
└── ANALYTICS_DB/
    └── 001_create_events.sql

Production 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); // 1

exec() - 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 executed

withSession() - 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 | null

Always use prepared statements with .bind() to prevent SQL injection.

Next Steps

  • Workers - Learn about Ploy workers
  • Queues - Add background job processing

How is this guide?

Last updated on