PlanetScale
PlanetScale is a serverless database platform built on Vitess (YouTube's database infrastructure). Created in 2018, PlanetScale brings Git-like workflows to databases with branching, non-blocking schema changes, and horizontal scaling. Originally MySQL-only, PlanetScale now also offers managed Postgres. For JavaScript/TypeScript developers, PlanetScale offers modern serverless features and a workflow designed for continuous deployment.
What is PlanetScale?
PlanetScale is serverless MySQL with database branching:
import { connect } from '@planetscale/database';
const conn = connect({
url: process.env.DATABASE_URL,
});
const results = await conn.execute('SELECT * FROM users WHERE email = ?', [
'[email protected]',
]);
console.log(results.rows);
Key Features:
- MySQL-compatible: Use any MySQL tool/library
- Database branching: Create database branches like Git
- Non-blocking schema changes: Deploy without downtime
- Serverless: Auto-scaling, pay-per-use
- Horizontal scaling: Powered by Vitess
- Connection pooling: Built-in (no separate pooler needed)
Why PlanetScale?
1. Database Branching
Create database branches like Git:
# Create development branch from main
pscale branch create my-database dev
# Each PR gets its own database
pscale branch create my-database pr-123
# Delete when done
pscale branch delete my-database pr-123
main branch: production data
↓
dev branch: development data (instant copy)
↓
pr-123 branch: preview data (instant copy)
Perfect for preview deployments!
2. Non-Blocking Schema Changes
Deploy schema changes without downtime:
# Create schema branch
pscale branch create my-database add-column
# Make changes
pscale shell my-database add-column
> ALTER TABLE users ADD COLUMN phone VARCHAR(20);
# Create deploy request
pscale deploy-request create my-database add-column
# Review and deploy (zero downtime)
pscale deploy-request deploy my-database 1
Schema changes apply without locking tables.
3. Serverless MySQL
Low traffic:
[Small compute] ← Minimal resources
High traffic:
[Large compute] ← Auto-scales
No traffic:
[Connection pooler] ← Maintains connections
Pay only for what you use.
4. Vitess-Powered
Built on Vitess (YouTube's database infrastructure):
- Horizontal sharding
- Connection pooling
- Query rewriting
- Automatic failover
Using PlanetScale with TypeScript
Setup
# Sign up at https://planetscale.com
# Create database
pscale database create my-database --region us-east
# Get connection string
pscale connect my-database main
# Or use dashboard to get connection string
With @planetscale/database (Recommended for Serverless)
pnpm add @planetscale/database
import { connect } from '@planetscale/database';
const conn = connect({
url: process.env.DATABASE_URL,
});
// Query
const results = await conn.execute(
'SELECT * FROM users WHERE email = ?',
['[email protected]']
);
console.log(results.rows);
// Insert
const insertResult = await conn.execute(
'INSERT INTO users (name, email) VALUES (?, ?)',
['Alice', '[email protected]']
);
console.log('Inserted ID:', insertResult.insertId);
// Transaction
const tx = await conn.transaction();
try {
await tx.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 1]);
await tx.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, 2]);
await tx.commit();
} catch (error) {
await tx.rollback();
throw error;
}
With Prisma
pnpm add @prisma/client
pnpm add -D prisma
// prisma/schema.prisma
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
// relationMode = "prisma" // Optional — PlanetScale now supports FK constraints natively
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(uuid())
name String
email String @unique
posts Post[]
createdAt DateTime @default(now())
@@index([email])
}
model Post {
id String @id @default(uuid())
title String
content String @db.Text
authorId String
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
@@index([authorId])
}
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Create user
const user = await prisma.user.create({
data: {
name: 'Alice',
email: '[email protected]',
posts: {
create: [
{ title: 'First Post', content: 'Hello World' },
],
},
},
include: {
posts: true,
},
});
console.log(user);
Note: PlanetScale now supports foreign key constraints. Older projects may still use relationMode = "prisma" to enforce referential integrity at the application level, but this is no longer required.
With mysql2
pnpm add mysql2
import mysql from 'mysql2/promise';
const pool = mysql.createPool(process.env.DATABASE_URL!);
// Query
const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', [
'[email protected]',
]);
console.log(rows);
// Insert
const [result] = await pool.query(
'INSERT INTO users (name, email) VALUES (?, ?)',
['Bob', '[email protected]']
);
console.log('Inserted ID:', result.insertId);
Database Branching Workflow
Development Workflow
# 1. Create dev branch
pscale branch create my-database dev
# 2. Get connection string for dev branch
pscale connect my-database dev
# 3. Make schema changes
pscale shell my-database dev
> CREATE TABLE posts (
> id VARCHAR(36) PRIMARY KEY,
> title VARCHAR(255),
> content TEXT
> );
# 4. Test in development
# 5. Create deploy request
pscale deploy-request create my-database dev
# 6. Deploy to main (zero downtime)
pscale deploy-request deploy my-database 1
Preview Deployment Workflow
# In CI/CD (GitHub Actions, etc.)
- name: Create preview branch
run: |
BRANCH_NAME="pr-${{ github.event.pull_request.number }}"
pscale branch create my-database $BRANCH_NAME --from main
DATABASE_URL=$(pscale connect my-database $BRANCH_NAME --format url)
echo "DATABASE_URL=$DATABASE_URL" >> $GITHUB_ENV
# Deploy preview with its own database
# Delete branch when PR is merged/closed
PlanetScale + Framework Integration
With Next.js
// lib/db.ts
import { PrismaClient } from '@prisma/client';
let prisma: PrismaClient;
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient();
} else {
if (!global.prisma) {
global.prisma = new PrismaClient();
}
prisma = global.prisma;
}
export default prisma;
// app/api/users/route.ts
import prisma from '@/lib/db';
import { NextResponse } from 'next/server';
export async function GET() {
const users = await prisma.user.findMany();
return NextResponse.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const user = await prisma.user.create({
data: body,
});
return NextResponse.json(user);
}
With Node.js/Express
import express from 'express';
import { PrismaClient } from '@prisma/client';
const app = express();
const prisma = new PrismaClient();
app.use(express.json());
app.get('/users', async (req, res) => {
const users = await prisma.user.findMany();
res.json(users);
});
app.post('/users', async (req, res) => {
const user = await prisma.user.create({
data: req.body,
});
res.json(user);
});
app.listen(3000);
Schema Changes (Deploy Requests)
Create Deploy Request
# 1. Create schema branch
pscale branch create my-database add-phone-column
# 2. Connect to branch
pscale shell my-database add-phone-column
# 3. Make changes
> ALTER TABLE users ADD COLUMN phone VARCHAR(20);
# 4. Create deploy request
pscale deploy-request create my-database add-phone-column
# 5. Review diff in dashboard
# 6. Deploy (zero downtime)
pscale deploy-request deploy my-database 1
Safe Schema Changes
PlanetScale enables non-blocking schema changes:
-- Add column (safe)
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Add index (safe)
CREATE INDEX idx_users_email ON users(email);
-- Remove column (requires multiple deploys)
-- 1. Stop using the column in code
-- 2. Deploy code changes
-- 3. Remove column via deploy request
ALTER TABLE users DROP COLUMN old_column;
Pricing
PlanetScale no longer offers a free tier. Pricing is based on provisioned compute cluster sizes for predictable monthly costs.
Postgres Single Node (Starting at $5/month):
- Postgres only — not available for MySQL/Vitess
- PS-5 node: 1/16 vCPU, 512 MB RAM, 10 GB storage
- No high availability (single instance, expect downtime if node fails)
- Best for hobby projects, dev environments, and testing
- Upgrade to HA (3 nodes) for $15/month at the same node size
Scaler Pro (Starting at $39/month):
- Available for both MySQL/Vitess and Postgres
- Highly available clusters (1 primary + 2 replicas across 3 AZs)
- Unmetered row reads/writes
- 10 GB storage included
- 1 production branch, ~1,440 hours of dev branching
- Scales by compute size: PS-10 ($39/mo) → PS-20 ($59/mo) → PS-40 ($99/mo) and up
- This is the cheapest option for MySQL/Vitess databases
PlanetScale Metal (Starting at $50/month):
- Local NVMe storage for low-latency workloads
- M-10 cluster with 10 GB NVMe at $50/mo
- Scales based on disk size and compute
Enterprise (Custom Pricing):
- Custom cluster sizes, SSO included
- Unlimited development branches
- 99.999% SLA for multi-region deployments
Additional Costs:
- Storage overages: $0.50/GB per instance ($1.50/GB effective for HA clusters)
- Egress: 100 GB included, then $0.06/GB
- SSO add-on: $199/month (standard plans)
- Extra dev branches: ~$0.014/hour or $5/month per Postgres dev branch
PlanetScale vs. Other Databases
| Feature | PlanetScale | Neon | Traditional MySQL |
|---|---|---|---|
| Database | MySQL + Postgres | PostgreSQL | MySQL |
| Branching | Yes | Yes | Manual |
| Serverless | Yes | Yes | No |
| Schema Changes | Non-blocking | Standard | Blocking |
| Horizontal Scale | Yes (Vitess) | Limited | Manual sharding |
| Starting Price | $5/month (Postgres), $39/month (MySQL) | Free tier | Self-hosted |
| Best For | High-scale apps | Postgres apps | Traditional hosting |
Limitations
- Foreign key constraints: Supported since 2024 (previously a Vitess limitation)
- No triggers: Not supported by Vitess
- No stored procedures: Not supported
- No full-text search: Use external search service
- No free tier: Postgres from $5/month, MySQL from $39/month
- Read-heavy: Write scaling more limited than reads
Best Practices
1. Use Indexes
-- Add indexes for common query patterns
CREATE INDEX idx_posts_author_id ON posts(author_id);
CREATE INDEX idx_posts_created_at ON posts(created_at);
-- Composite indexes for common queries
CREATE INDEX idx_posts_author_created ON posts(author_id, created_at);
2. Use Foreign Key Constraints
PlanetScale now supports foreign key constraints natively. For older projects, you can still use relationMode = "prisma" in your Prisma schema if preferred.
3. Use Branches for Testing
# Test migrations on branch first
pscale branch create my-database test-migration
pscale shell my-database test-migration
> -- Test your migration here
# If successful, deploy to main
pscale deploy-request create my-database test-migration
4. Use Connection Pooling
// @planetscale/database has built-in pooling
// No additional pooler needed
import { connect } from '@planetscale/database';
const conn = connect({ url: process.env.DATABASE_URL });
Key Takeaways
- Serverless MySQL and Postgres with database branching
- Non-blocking schema changes (zero downtime deploys)
- Powered by Vitess (YouTube's database infrastructure)
- No free tier: Postgres from $5/month (single node), MySQL/Vitess from $39/month
- Foreign key constraints: Now supported natively
- MySQL and Postgres: Both database engines available
- Perfect for: High-scale apps with preview deployments
- Best workflow: Branch for every PR
Related Topics
- MySQL - Database technology behind PlanetScale
- Neon - Alternative for PostgreSQL
- Databases Overview - Compare all databases
- Next.js - Popular framework for PlanetScale apps
- Prisma - Recommended ORM for PlanetScale
PlanetScale is an excellent choice for applications needing modern serverless features and database branching. With support for both MySQL (via Vitess) and Postgres, its workflow is designed for continuous deployment, making it perfect for teams using preview deployments. Use it when you need Git-like database workflows at scale.