Use PlanetScale with DaloyJS
PlanetScale is a managed MySQL host built around Vitess, branching, deploy requests, and a fetch-based HTTP driver. Because @planetscale/database uses plain fetch, it runs on every runtime DaloyJS supports — including Cloudflare Workers and Vercel Edge. If you are using PlanetScale Postgres, follow the Neon driver pattern instead.
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.
2. Install
pnpm add @planetscale/database3. Create a PlanetScale plugin
// src/db/planetscale.ts
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;
export const planetscalePlugin = {
name: "planetscale",
register(app: App) {
app.decorate("db", db);
},
};4. Augment app state
// src/types/state.d.ts
import type { Db } from "../db/planetscale";
declare module "@daloyjs/core" {
interface AppState {
db: Db;
}
}5. Use it in a route
import { z } from "zod";
import { App, secureHeaders } from "@daloyjs/core";
import { planetscalePlugin } from "./db/planetscale";
const app = new App();
app.use(secureHeaders());
app.register(planetscalePlugin);
const UserSchema = z.object({ id: z.string(), email: z.string().email() });
app.route({
method: "GET",
path: "/users/:id",
operationId: "getUser",
request: { params: z.object({ id: z.string() }) },
responses: {
200: { description: "Found", body: UserSchema },
404: { description: "Not found" },
},
handler: async ({ params, state }) => {
const result = await state.db.execute(
"select id, email from users where id = ? limit 1",
[params.id],
);
const row = result.rows[0] as { id: string; email: string } | undefined;
return row
? { status: 200, body: row }
: { status: 404, body: { type: "about:blank", title: "Not found", status: 404 } };
},
});Cloudflare Workers
Construct the connection inside the worker handler so it picks up the binding from env, then call app.fetch(req). If your app does not need worker bindings, you can export the standard Cloudflare adapterdirectly.
import { connect } from "@planetscale/database";
export default {
async fetch(
req: Request,
env: { DATABASE_HOST: string; DATABASE_USERNAME: string; DATABASE_PASSWORD: string },
) {
const db = connect({
host: env.DATABASE_HOST,
username: env.DATABASE_USERNAME,
password: env.DATABASE_PASSWORD,
});
app.decorate("db", db);
return app.fetch(req);
},
};With Drizzle ORM
pnpm add drizzle-orm
// src/db/drizzle.ts
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
Use the PlanetScale Driver Adapter (GA since Prisma 6.16.0). PlanetScale disables foreign-key constraints by default on MySQL unless you enable them in database settings, so set relationMode = "prisma"in your schema.prisma when you are using the default no-FK mode, and point DATABASE_URL at the serverless host (aws.connect.psdb.cloud).
pnpm add @prisma/adapter-planetscale
// src/db/prisma.ts
import { PrismaClient } from "@prisma/client";
import { PrismaPlanetScale } from "@prisma/adapter-planetscale";
const adapter = new PrismaPlanetScale({ url: process.env.DATABASE_URL! });
export const prisma = new PrismaClient({ adapter });On Node.js versions older than 18 (no global fetch), install undici and pass { fetch: undiciFetch } as a second option.
Branching & deploy requests
PlanetScale'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 — just swap the host.
See also Neon, Supabase, and the database hosting overview.