PlanetScale MySQL, built on Vitess, with branching, deploy requests, and the fetch-based @planetscale/database HTTP driver. Because that driver uses plain fetch, it runs on Node, Bun, Deno, Lambda, and Cloudflare Workers. Most of this page covers it.
PlanetScale Postgres, which is Postgres: use a standard driver such as pg. @planetscale/database is the MySQL (Vitess) driver and is not used for Postgres. Jump to PlanetScale Postgres.
One HTTP driver, every runtime
DaloyJS routehandler reads state.db
app.decorate("db", db)db.execute(sql, params)
@planetscale/databasefetch-based HTTP driver
NodeBunDenoLambdaWorkers
PlanetScale MySQLmanaged MySQL on Vitess
branchesdeploy requests
Because @planetscale/database speaks plain fetch instead of a raw TCP socket, the same data-access code runs on every runtime DaloyJS targets, including Cloudflare Workers.
1. Provision and grab credentials
Create a database at app.planetscale.com, generate a password, and copy the host plus credentials. Set them as DATABASE_HOST, DATABASE_USERNAME, and DATABASE_PASSWORD. Give the app a password with the read/write role, not admin.
2. Install
bash
pnpm add @planetscale/database@^1
bash
npm install @planetscale/database@^1
bash
yarn add @planetscale/database@^1
bash
bun add @planetscale/database@^1
3. Create a PlanetScale plugin
src/db/planetscale.tsts
import { connect } from "@planetscale/database";import type { App } from "@daloyjs/core";export const db = connect({ host: process.env.DATABASE_HOST!, username: process.env.DATABASE_USERNAME!, password: process.env.DATABASE_PASSWORD!,});export type Db = typeof db;// Call with the root app, not through app.register(): a decoration made// inside a registered plugin is visible only to that plugin's own routes.export function planetscalePlugin(app: App) { app.decorate("db", db);}
4. Augment app state
Add the declare module block to the same module that creates the client, not to a separate .d.ts file. Declaration files are exempt from type-checking when skipLibCheck is on (the scaffolded default), so a broken import inside a .d.ts fails silently and state.db degrades to any.
ts
// src/db/planetscale.ts (same module as the plugin above)declare module "@daloyjs/core" { interface AppState { db: Db; }}
Store the credentials with pnpm wrangler secret put, run pnpm wrangler types so they are typed on env, construct the connection from env (imported from cloudflare:workers), and decorate the app before registering routes. Do not decorate inside the Worker fetch handler: decorate() throws on the second request, and a decoration added after the routes were registered never reaches them.
src/index.tsts
import { env } from "cloudflare:workers";import { connect } from "@planetscale/database";import { App } from "@daloyjs/core";import { toFetchHandler } from "@daloyjs/core/cloudflare";const app = new App({ production: true, behindProxy: { hops: 1 } });app.decorate( "db", connect({ host: env.DATABASE_HOST, username: env.DATABASE_USERNAME, password: env.DATABASE_PASSWORD, }),);// ...register routes that read state.dbexport default toFetchHandler(app);
If you would rather use mysql2 with a regular MySQL connection string, put Hyperdrive in front of the database; Cloudflare lists PlanetScale among its Hyperdrive providers.
With Drizzle ORM
bash
pnpm add drizzle-orm
bash
npm install drizzle-orm
bash
yarn add drizzle-orm
bash
bun add drizzle-orm
src/db/drizzle.tsts
import { drizzle } from "drizzle-orm/planetscale-serverless";export const db = drizzle({ connection: { host: process.env.DATABASE_HOST!, username: process.env.DATABASE_USERNAME!, password: process.env.DATABASE_PASSWORD!, },});
With Prisma
Prisma 7 connects through a driver adapter, which is mandatory, and generates the client into your source tree. Use the PlanetScale adapter (current release: Prisma 7.10):
bun add @prisma/client @prisma/adapter-planetscale
bash
pnpm add -D prisma
bash
npm install --save-dev prisma
bash
yarn add --dev prisma
bash
bun add --dev prisma
PlanetScale MySQL disables foreign-key constraints by default unless you enable them in the database settings. In the default no-FK mode, set relationMode = "prisma" and add your own indexes on relation fields:
import { PrismaPlanetScale } from "@prisma/adapter-planetscale";import { PrismaClient } from "../generated/prisma/client.ts";const adapter = new PrismaPlanetScale({ url: process.env.DATABASE_URL! });export const prisma = new PrismaClient({ adapter });
Prisma CLI commands read their connection URL from prisma.config.ts. See Prisma for the config file, plugin, and shutdown setup.
PlanetScale Postgres
PlanetScale Postgres is a Postgres database, so you use pg (or any Postgres driver or ORM) with the connection string from the dashboard. TLS is required. PlanetScale recommends port 6432 (PgBouncer, transaction pooling) for application traffic and port 5432 (direct) for migrations, long-running admin work, and session features.
bash
pnpm add pg
bash
npm install pg
bash
yarn add pg
bash
bun add pg
bash
pnpm add -D @types/pg
bash
npm install --save-dev @types/pg
bash
yarn add --dev @types/pg
bash
bun add --dev @types/pg
Node, Bun, Deno, and Lambda
src/db/pg.tsts
import { Pool } from "pg";import type { App } from "@daloyjs/core";// Module scope: reused across requests and warm Lambda invocations.export const pool = new Pool({ connectionString: process.env.DATABASE_URL!, // the :6432 PgBouncer string max: 10, idleTimeoutMillis: 30_000,});declare module "@daloyjs/core" { interface AppState { db: Pool; }}// Call with the root app, not through app.register().export function pgPlugin(app: App) { app.decorate("db", pool); // Runs on the Node, Bun, and Deno adapters. Lambda has no shutdown hook, // so keep max small there and rely on the idle timeout. app.onClose(async () => { await pool.end(); });}
Query with parameters: await state.db.query("select id, email from users where id = $1", [params.id]). PgBouncer in transaction mode does not keep session state, so avoid SET, advisory locks, and LISTEN on the 6432 string.
Cloudflare Workers
Create a Hyperdrive config with the PlanetScale Postgres connection string and use pg with env.HYPERDRIVE.connectionString, one client per request. The Hyperdrive page has the full DaloyJS sample.
Branching & deploy requests
PlanetScale MySQL's schema workflow uses branches and deploy requests rather than ad-hoc ALTER TABLE. Pair this with your CI: run migrations against a development branch, open a deploy request, and merge to main. The same Daloy app code works against any branch. Swap the host.