Ploy
Ploy
Next.js

Database

Use SQLite databases in your Next.js app with Ploy.

Database

Add SQLite databases to your Next.js application with Ploy.

Generated bindings work directly in getPloyContext(), so existing SQL access patterns transfer cleanly.

Configuration

Add a database binding in your ploy.yaml:

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

Generate types:

pnpm ploy types

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/
migrations/
└── 20260403171410_init/
    ├── migration.sql
    └── snapshot.json

Only .sql files are executed, so sibling files such as snapshot.json are ignored.

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

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

Production 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.

API Route Example

This example assumes your users table already exists from a migration in migrations/.

app/api/users/route.ts
import { NextResponse } from "next/server";
import { getPloyContext } from "@meetploy/nextjs";

interface User {
	id: number;
	name: string;
	email: string;
}

export async function GET() {
	const { env } = getPloyContext();

	const result = await env.DB.prepare("SELECT * FROM users").all<User>();

	return NextResponse.json({ users: result.results });
}

export async function POST(request: Request) {
	const { env } = getPloyContext();
	const { name, email } = await request.json();

	const result = await env.DB.prepare(
		"INSERT INTO users (name, email) VALUES (?, ?)",
	)
		.bind(name, email)
		.run();

	return NextResponse.json({ success: true, meta: result.meta });
}

Server Component Example

app/users/page.tsx
import { getPloyContext } from "@meetploy/nextjs";

interface User {
  id: number;
  name: string;
}

export default async function UsersPage() {
  const { env } = getPloyContext();

  const { results: users } = await env.DB.prepare(
    "SELECT * FROM users"
  ).all<User>();

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Query Methods

all() - Get All Rows

const { results } = await env.DB.prepare("SELECT * FROM users").all<User>();

first() - Get First Row

const user = await env.DB.prepare("SELECT * FROM users WHERE id = ?")
	.bind(1)
	.first<User>();

run() - Execute Statement

const result = await env.DB.prepare("INSERT INTO users (name) VALUES (?)")
	.bind("Alice")
	.run();

exec() - Run Raw SQL

const result = await env.DB.exec(`
  CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
  PRAGMA optimize;
`);

console.log(result.count);

withSession() - Session Compatibility

const session = env.DB.withSession("first-primary");
const user = await session
	.prepare("SELECT * FROM users WHERE id = ?")
	.bind(1)
	.first<User>();

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

Local Development

During development, the local Ploy dashboard at http://localhost:4000 lets you:

  • Browse table contents
  • Execute SQL queries
  • Inspect database structure

Project migrations are applied on startup, and data persists across restarts in the .ploy directory.

Next Steps

  • Queues - Process background jobs
  • Workflows - Build durable multi-step processes

How is this guide?

Last updated on