PlanetScale Serverless MySQL Patterns
Quick Guide: Use
@planetscale/databasefor edge/serverless MySQL access via HTTP (Fetch API). UseClientto create per-request connections,conn.execute()for parameterized queries, andconn.transaction()for atomic operations. Never run DDL directly on production -- use deploy requests with safe migrations enabled. PlanetScale runs on Vitess: foreign keys are supported but opt-in, stored procedures are not supported, and all schema changes go through online DDL. The built-incasthandles regular integers and floats automatically, but provide a customcastfor BigInt, Date, and boolean columns. Branch your database like git branches for dev/preview environments.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use conn.execute(sql, params) with parameterized queries -- never interpolate user input into SQL strings)
(You MUST use deploy requests for ALL schema changes on production branches with safe migrations enabled -- direct DDL is rejected)
(You MUST create a fresh Client.connection() per request in serverless environments -- do not reuse connections across invocations)
(You MUST handle the Vitess/MySQL compatibility differences: no stored procedures, no RENAME COLUMN via direct DDL, no := operator, no LOAD DATA INFILE)
(You MUST provide a custom cast function for BigInt (INT64/UINT64), Date (DATETIME/TIMESTAMP), and boolean (TINYINT(1)) columns -- the default cast handles regular integers and floats but leaves these as strings)
</critical_requirements>
Auto-detection: PlanetScale, @planetscale/database, planetscale serverless driver, pscale, deploy request, safe migrations, Vitess, database branching, planetscale branch, planetscale boost, mysql serverless, pscale CLI, planetscale connection
When to use:
- Querying MySQL from edge/serverless functions via the PlanetScale serverless driver
- Managing schema changes through deploy requests and safe migrations
- Creating database branches for dev, preview, or CI environments
- Setting up connections with
@planetscale/database(host/username/password or URL) - Running transactions in serverless contexts
- Handling Vitess-specific SQL compatibility constraints
- Programmatic branch management via
pscaleCLI
Key patterns covered:
connect()/Clientconnection setup with host, username, passwordconn.execute()with positional (?) and named (:param) parametersconn.transaction()for atomic multi-statement operations- Custom
castfunctions for type-safe value conversion (BigInt, Date, boolean) - Deploy request workflow (branch, change schema, create DR, review, deploy)
- Safe migrations and the no-direct-DDL enforcement model
- Database branching for dev/preview/CI environments
- Vitess SQL compatibility constraints and workarounds
pscaleCLI for branch and deploy request management
When NOT to use:
- Long-running server processes with persistent TCP MySQL connections (use
mysql2driver) - Complex ORM-specific patterns (use your ORM's own skill)
- General MySQL query syntax (use a SQL/MySQL skill)
- PostgreSQL workloads (use Neon or another Postgres provider)
Detailed Resources:
- For decision frameworks, CLI reference, and quick lookup tables, see reference.md
Driver & Queries:
- examples/core.md -- Connection setup, parameterized queries, transactions, type casting
Branching & Schema Changes:
- examples/branching.md -- Dev branches, deploy requests, safe migrations, pscale CLI, CI/CD workflows
<philosophy>
Philosophy
PlanetScale is a serverless MySQL platform built on Vitess, the same technology that powers YouTube's database infrastructure. The @planetscale/database driver uses HTTP (Fetch API) instead of TCP, making MySQL accessible from edge runtimes that lack TCP support.
Core principles:
- HTTP-based, stateless connections -- Every query is an HTTP request. There are no persistent connections to manage, no connection pools to configure. Create a connection, execute queries, done. PlanetScale handles connection pooling at the infrastructure level (Vitess VTTablet + Global Routing).
- Schema changes via deploy requests, never direct DDL -- Production branches with safe migrations reject direct
CREATE,ALTER,DROPstatements. All schema changes go through deploy requests: branch, modify schema on the branch, create a deploy request, review the diff, deploy with zero downtime via online DDL. - Branches are cheap -- Database branches are isolated copies of your schema (and optionally data). Create them for feature development, PR previews, CI runs. Delete when done.
- Vitess under the hood -- PlanetScale runs Vitess, which adds horizontal scaling but introduces SQL compatibility differences. No stored procedures, no
RENAME COLUMNin DDL, no:=operator. Foreign keys are supported but opt-in and come with performance trade-offs. - Default cast handles common types, customize for the rest -- The driver's built-in
castfunction automatically converts INT8-32 and FLOAT32/64 to JavaScript numbers, and parses JSON. However, INT64/UINT64 (BigInt), DATETIME/TIMESTAMP (Date), DECIMAL, and TINYINT(1) (boolean) remain as strings -- provide a customcastfunction for these.
When to use PlanetScale serverless driver:
- Edge/serverless functions that cannot open TCP connections
- Applications using PlanetScale's branching and deploy request workflow
- High-concurrency serverless apps benefiting from PlanetScale's infrastructure-level pooling
- Teams wanting git-like database workflows (branch, review, merge)
When NOT to use:
- Long-running server processes (use
mysql2with TCP for persistent connections) - Workloads requiring stored procedures, triggers, or events (Vitess does not support them)
- Applications requiring
LOAD DATA INFILE(not supported)
<patterns>
Core Patterns
Pattern 1: Connection Setup
The driver provides two connection methods: connect() for a single connection and Client for a connection factory. Use Client in serverless (fresh connection per request), connect() for single long-lived connection objects.
import { connect } from "@planetscale/database";
const conn = connect({
host: process.env.DATABASE_HOST!,
username: process.env.DATABASE_USERNAME!,
password: process.env.DATABASE_PASSWORD!,
});
const { rows } = await conn.execute(
"SELECT id, name FROM users WHERE active = ?",
[true],
);
See examples/core.md for full connection patterns including Client factory, URL-based config, and custom fetch for HTTP/2.
Pattern 2: Parameterized Queries
The driver supports positional (?) and named (:param) parameter styles. Both are auto-escaped preventing SQL injection. Never mix styles in a single execute() call.
// Positional: array of values
await conn.execute("SELECT id, name FROM users WHERE id = ? AND active = ?", [
userId,
true,
]);
// Named: object of values
await conn.execute("SELECT id, name FROM users WHERE role = :role", {
role: "admin",
});
See examples/core.md for complex named parameter queries and bad examples to avoid.
Pattern 3: Transactions
conn.transaction() executes multiple queries atomically with automatic rollback on error. Each tx.execute() is an HTTP round trip, but conditional logic runs client-side within the callback.
const result = await conn.transaction(async (tx) => {
const debit = await tx.execute(
"UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?",
[amount, fromId, amount],
);
if (debit.r