PlanetScale - Kompletny Przewodnik po Serverless MySQL
Czym jest PlanetScale?
PlanetScale to serverless MySQL database zbudowana na Vitess - tej samej technologii, która zasila YouTube i obsługuje miliardy zapytań dziennie. PlanetScale wprowadza rewolucyjne podejście do zarządzania bazami danych poprzez branching (jak w Git), zero-downtime schema changes i automatyczne skalowanie.
Tradycyjne bazy danych wymagają ręcznego zarządzania schematami, planowania downtime na migracje i złożonej infrastruktury do skalowania. PlanetScale eliminuje te problemy, oferując doświadczenie developera znane z nowoczesnych narzędzi jak GitHub - tworzysz branch, wprowadzasz zmiany, otwierasz deploy request i mergeujesz bez przestojów.
Dlaczego PlanetScale?
Kluczowe zalety PlanetScale
- Database Branching - Izolowane środowiska do developmentu i testów
- Zero-downtime schema changes - Migracje bez przestojów produkcji
- Automatyczne skalowanie - Od hobby do enterprise bez konfiguracji
- Vitess pod spodem - Sprawdzona technologia YouTube
- Serverless - Płać za użycie, nie za idle
- Edge-ready - Niskie latencje globalnie
- Schema insights - AI-powered rekomendacje
- Connection pooling - Wbudowany PlanetScale Proxy
PlanetScale vs Tradycyjne MySQL
| Cecha | PlanetScale | MySQL RDS | MySQL self-hosted |
|---|---|---|---|
| Branching | Tak | Nie | Nie |
| Schema changes | Zero downtime | Downtime | Downtime |
| Skalowanie | Automatyczne | Manualne | Manualne |
| Repliki | Automatyczne | Manualne | Manualne |
| Foreign keys | Prisma relations | Native | Native |
| Connection pooling | Wbudowany | Dodatkowy | Dodatkowy |
| Backupy | Automatyczne | Manualne | Manualne |
| Cena | Usage-based | Instance-based | Hardware |
PlanetScale vs Inne Serverless DB
| Cecha | PlanetScale | Neon | Supabase | CockroachDB |
|---|---|---|---|---|
| Engine | MySQL/Vitess | PostgreSQL | PostgreSQL | Custom |
| Branching | Tak | Tak | Nie | Nie |
| Free tier | 5GB | 3GB | 500MB | 5GB |
| Edge | Tak | Tak | Nie | Tak |
| Schema changes | Zero downtime | Downtime | Downtime | Zero downtime |
Kiedy wybrać PlanetScale?
- Potrzebujesz MySQL - Nie PostgreSQL
- Częste schema changes - CI/CD dla bazy danych
- Globalna aplikacja - Edge locations
- Team workflow - Branching i review
- Zero-downtime - Krytyczna produkcja
Instalacja i Konfiguracja
PlanetScale CLI
# macOS
brew install planetscale/tap/pscale
# Linux
curl -sSL https://github.com/planetscale/cli/releases/latest/download/pscale_linux_amd64.tar.gz | tar xz
sudo mv pscale /usr/local/bin/
# Windows (scoop)
scoop bucket add pscale https://github.com/planetscale/scoop-bucket.git
scoop install pscale
# Weryfikacja
pscale versionLogowanie i tworzenie bazy
# Zaloguj się
pscale auth login
# Stwórz nową bazę
pscale database create my-app --region us-east
# Lista dostępnych regionów
pscale region list
# Lista baz
pscale database listDostępne regiony
| Region | Kod | Lokalizacja |
|---|---|---|
| US East | us-east | N. Virginia |
| US West | us-west | Oregon |
| EU West | eu-west | Dublin |
| Asia Pacific | ap-south | Mumbai |
| Asia Pacific | ap-northeast | Tokyo |
| Australia | ap-southeast | Sydney |
| South America | sa-east | São Paulo |
Database Branching
Koncepcja branchingu
PlanetScale traktuje bazę danych jak kod - możesz tworzyć izolowane branche do pracy nad nowymi features bez wpływu na produkcję:
# Stwórz branch do developmentu
pscale branch create my-app feature-auth
# Lista branchy
pscale branch list my-app
# Przełącz się na branch
pscale branch show my-app feature-auth
# Usuń branch
pscale branch delete my-app feature-authWorkflow z branchami
# 1. Stwórz branch dla feature
pscale branch create my-app add-user-profile
# 2. Połącz się z branchem lokalnie
pscale connect my-app add-user-profile --port 3309
# 3. Wprowadź zmiany w schemacie
mysql -h 127.0.0.1 -P 3309 -u root < migrations/add_profile.sql
# 4. Stwórz deploy request (jak PR)
pscale deploy-request create my-app add-user-profile
# 5. Przeglądaj zmiany
pscale deploy-request diff my-app 1
# 6. Deploy do main (merge)
pscale deploy-request deploy my-app 1Branch Types
# Development branch - do pracy
pscale branch create my-app dev
# Staging branch - do testów
pscale branch create my-app staging
# Production branch - main (domyślny)
# Chroniony, wymaga deploy request
# Feature branch - krótkotrwały
pscale branch create my-app feature-xyz --from stagingSafe Migrations
PlanetScale automatycznie wykrywa niebezpieczne migracje:
-- ✅ Bezpieczne - dodanie kolumny
ALTER TABLE users ADD COLUMN bio TEXT;
-- ✅ Bezpieczne - dodanie indeksu
CREATE INDEX idx_users_email ON users(email);
-- ⚠️ Wymaga uwagi - zmiana typu kolumny
ALTER TABLE users MODIFY COLUMN age BIGINT;
-- ❌ Niebezpieczne - usunięcie kolumny z danymi
ALTER TABLE users DROP COLUMN legacy_field;Zero-Downtime Schema Changes
Jak to działa?
PlanetScale używa Online Schema Change (OSC) inspirowanego gh-ost:
- Shadow table - Tworzy kopię tabeli z nowym schematem
- Data copy - Kopiuje dane w tle
- Changelog - Śledzi zmiany podczas kopiowania
- Atomic swap - Przełącza tabele atomicznie
- Cleanup - Usuwa starą tabelę
-- Twój DDL
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(500);
-- PlanetScale wykonuje:
-- 1. CREATE TABLE _users_new LIKE users;
-- 2. ALTER TABLE _users_new ADD COLUMN avatar_url...
-- 3. INSERT INTO _users_new SELECT * FROM users (chunked)
-- 4. RENAME TABLE users TO _users_old, _users_new TO users;
-- 5. DROP TABLE _users_old;
-- Zero downtime dla aplikacji!Deploy Requests
# Stwórz deploy request
pscale deploy-request create my-app feature-auth
# Pokaż diff
pscale deploy-request diff my-app 1
# Output:
# -- users table
# + avatar_url VARCHAR(500)
# + bio TEXT
# -- INDEX changes
# + idx_users_email
# Dodaj komentarz
pscale deploy-request comment my-app 1 "Reviewed, LGTM"
# Deploy
pscale deploy-request deploy my-app 1
# Śledź postęp
pscale deploy-request show my-app 1Schema w różnych środowiskach
# Compare branches
pscale branch diff my-app main feature-auth
# Promote staging to production
pscale deploy-request create my-app staging --into main
# Revert (stwórz nowy deploy request z odwrotnymi zmianami)
pscale deploy-request revert my-app 1Połączenie z Aplikacją
Connection Strings
# Wygeneruj connection string
pscale password create my-app main production-password
# Output:
# HOST: aws.connect.psdb.cloud
# USERNAME: xxxxx
# PASSWORD: pscale_pw_xxxxx
# DATABASE: my-app
# Format connection string
# mysql://USERNAME:PASSWORD@HOST/DATABASE?ssl={"rejectUnauthorized":true}Lokalne połączenie (development)
# Otwórz tunel do brancha
pscale connect my-app main --port 3306
# Teraz możesz połączyć się lokalnie
mysql -h 127.0.0.1 -P 3306 -u root
# Lub z aplikacji
DATABASE_URL="mysql://root@127.0.0.1:3306/my-app"Prisma ORM
npm install prisma @prisma/client
npx prisma init// prisma/schema.prisma
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma" // Wymagane dla PlanetScale!
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
}
model Post {
id String @id @default(cuid())
title String
content String? @db.Text
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
}# .env
DATABASE_URL='mysql://xxxxx:pscale_pw_xxxxx@aws.connect.psdb.cloud/my-app?sslaccept=strict'# Push schema do PlanetScale
npx prisma db push
# Generuj client
npx prisma generate// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}// Użycie
import { prisma } from '@/lib/prisma'
// Create
const user = await prisma.user.create({
data: {
email: 'user@example.com',
name: 'John Doe'
}
})
// Read with relations
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: true }
})
// Update
await prisma.user.update({
where: { id: user.id },
data: { name: 'Jane Doe' }
})
// Delete
await prisma.post.delete({
where: { id: postId }
})Drizzle ORM
npm install drizzle-orm @planetscale/database
npm install -D drizzle-kit// db/schema.ts
import { mysqlTable, varchar, text, boolean, timestamp, index } from 'drizzle-orm/mysql-core'
export const users = mysqlTable('users', {
id: varchar('id', { length: 128 }).primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 255 }),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
}, (table) => ({
emailIdx: index('email_idx').on(table.email)
}))
export const posts = mysqlTable('posts', {
id: varchar('id', { length: 128 }).primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
content: text('content'),
published: boolean('published').default(false),
authorId: varchar('author_id', { length: 128 }).notNull(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
}, (table) => ({
authorIdx: index('author_idx').on(table.authorId)
}))// db/index.ts
import { drizzle } from 'drizzle-orm/planetscale-serverless'
import { connect } from '@planetscale/database'
import * as schema from './schema'
const connection = connect({
host: process.env.DATABASE_HOST,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD
})
export const db = drizzle(connection, { schema })// Użycie
import { db } from '@/db'
import { users, posts } from '@/db/schema'
import { eq, and } from 'drizzle-orm'
// Create
const [user] = await db.insert(users).values({
id: crypto.randomUUID(),
email: 'user@example.com',
name: 'John'
}).$returningId()
// Read
const allUsers = await db.select().from(users)
// Read with conditions
const publishedPosts = await db
.select()
.from(posts)
.where(eq(posts.published, true))
// Join
const postsWithAuthors = await db
.select({
post: posts,
author: users
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
// Update
await db
.update(users)
.set({ name: 'Jane' })
.where(eq(users.id, userId))
// Delete
await db.delete(posts).where(eq(posts.id, postId))PlanetScale Serverless Driver
npm install @planetscale/database// Bezpośrednie użycie driver'a
import { connect } from '@planetscale/database'
const config = {
host: process.env.DATABASE_HOST,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD
}
const conn = connect(config)
// Simple query
const results = await conn.execute('SELECT * FROM users WHERE id = ?', [userId])
console.log(results.rows)
// Insert
await conn.execute(
'INSERT INTO users (id, email, name) VALUES (?, ?, ?)',
[crypto.randomUUID(), 'user@example.com', 'John']
)
// Transaction (pseudo - PlanetScale nie wspiera natywnych transactions)
// Użyj Prisma lub Drizzle dla transaction-like behavior
// Batch queries
const [users, posts] = await Promise.all([
conn.execute('SELECT * FROM users'),
conn.execute('SELECT * FROM posts WHERE published = 1')
])Foreign Keys w PlanetScale
Dlaczego brak natywnych FK?
PlanetScale używa Vitess, który nie wspiera foreign key constraints dla lepszego skalowania. Zamiast tego używaj:
- Application-level validation - Waliduj w kodzie
- Prisma relationMode - Emulowane relacje
- Database triggers (ograniczone wsparcie)
Prisma Relations
// schema.prisma
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma" // Emuluje FK w aplikacji
}
model User {
id String @id
posts Post[]
}
model Post {
id String @id
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
@@index([authorId]) // WAŻNE: Dodaj indeks ręcznie!
}Manualna walidacja
// Bez ORM - ręczna walidacja
async function createPost(authorId: string, title: string) {
// Sprawdź czy autor istnieje
const author = await conn.execute(
'SELECT id FROM users WHERE id = ?',
[authorId]
)
if (author.rows.length === 0) {
throw new Error('Author not found')
}
// Teraz stwórz post
await conn.execute(
'INSERT INTO posts (id, author_id, title) VALUES (?, ?, ?)',
[crypto.randomUUID(), authorId, title]
)
}Insights i Analytics
Query Insights
PlanetScale dashboard pokazuje:
- Slow queries - Zapytania powyżej progu
- Query patterns - Najczęstsze zapytania
- Index suggestions - Rekomendacje indeksów
- Connection stats - Aktywne połączenia
# CLI insights
pscale database show my-app --web
# Otwiera dashboard w przeglądarceSchema Recommendations
-- PlanetScale sugeruje:
-- ⚠️ Missing index detected
-- Query: SELECT * FROM posts WHERE author_id = ?
-- Recommendation:
CREATE INDEX idx_posts_author_id ON posts(author_id);
-- ⚠️ Unused index detected
-- Index: idx_legacy_field
-- Recommendation:
DROP INDEX idx_legacy_field ON users;Performance Schema
-- Włącz w dashboard lub:
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;Backup i Recovery
Automatyczne backupy
# Lista backupów
pscale backup list my-app
# Restore do nowego brancha
pscale backup restore my-app backup-id --branch restored-data
# Eksport backup
pscale backup download my-app backup-idPoint-in-time Recovery
# Przywróć do konkretnego momentu (Enterprise)
pscale backup restore my-app \
--timestamp "2024-01-15T10:30:00Z" \
--branch pitr-restoreManual Export
# Eksport przez mysqldump
pscale connect my-app main --port 3306 &
mysqldump -h 127.0.0.1 -P 3306 -u root my-app > backup.sql
# Import
mysql -h 127.0.0.1 -P 3306 -u root my-app < backup.sqlIntegracje
Vercel
# Zainstaluj integrację w Vercel Dashboard
# Lub ręcznie:
vercel env add DATABASE_URL production
vercel env add DATABASE_URL previewNext.js API Route
// app/api/users/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'
export async function GET() {
const users = await prisma.user.findMany({
take: 10,
orderBy: { createdAt: 'desc' }
})
return NextResponse.json(users)
}
export async function POST(request: Request) {
const data = await request.json()
const user = await prisma.user.create({
data: {
email: data.email,
name: data.name
}
})
return NextResponse.json(user, { status: 201 })
}Edge Functions
// app/api/edge/route.ts
import { connect } from '@planetscale/database'
export const runtime = 'edge'
const conn = connect({
host: process.env.DATABASE_HOST!,
username: process.env.DATABASE_USERNAME!,
password: process.env.DATABASE_PASSWORD!
})
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
const result = await conn.execute(
'SELECT * FROM users WHERE id = ?',
[id]
)
return Response.json(result.rows[0])
}GitHub Actions CI/CD
# .github/workflows/db-migrate.yml
name: Database Migration
on:
push:
branches: [main]
paths: ['prisma/schema.prisma']
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: planetscale/setup-pscale-action@v1
with:
version: latest
- name: Authenticate
run: pscale auth login --service-token ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
- name: Create branch
run: pscale branch create ${{ secrets.PLANETSCALE_DB }} pr-${{ github.run_id }}
- name: Push schema
run: |
npx prisma db push --accept-data-loss
env:
DATABASE_URL: ${{ secrets.DATABASE_URL_BRANCH }}
- name: Create deploy request
run: |
pscale deploy-request create ${{ secrets.PLANETSCALE_DB }} pr-${{ github.run_id }} \
--org ${{ secrets.PLANETSCALE_ORG }}Skalowanie i Performance
Read Replicas
# PlanetScale automatycznie routuje:
# - Writes -> Primary
# - Reads -> Replicas (gdy włączone)
# Włącz read replicas w dashboard
# Settings -> Read Replicas -> EnableConnection Pooling
// PlanetScale Proxy automatycznie pooluje połączenia
// Dla aplikacji serverless to krytyczne!
// Bez poolingu: każdy request = nowe połączenie
// Z PlanetScale: połączenia są reused
const config = {
host: process.env.DATABASE_HOST, // aws.connect.psdb.cloud
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
// Proxy automatycznie zarządza pool
}Query Optimization
-- Używaj EXPLAIN
EXPLAIN SELECT * FROM posts WHERE author_id = 'xxx';
-- Dodawaj indeksy
CREATE INDEX idx_posts_author_published
ON posts(author_id, published);
-- Unikaj SELECT *
SELECT id, title, created_at FROM posts WHERE author_id = ?;
-- Używaj LIMIT
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20;
-- Batch operations
INSERT INTO posts (id, title, author_id)
VALUES
(?, ?, ?),
(?, ?, ?),
(?, ?, ?);Cennik
Hobby (Free)
- Storage: 5GB
- Row reads: 1 miliard/miesiąc
- Row writes: 10 milionów/miesiąc
- Branches: 1 produkcyjny + 1 dev
- Backupy: Codzienne
Scaler ($29/miesiąc)
- Storage: 10GB (+ $2.50/GB)
- Row reads: 100 miliardów/miesiąc
- Row writes: 50 milionów/miesiąc
- Branches: Unlimited
- Backupy: Co godzinę
- Read replicas: 1 region
Team ($39/użytkownik/miesiąc)
- Wszystko z Scaler
- Audit logs
- SSO/SAML
- SOC 2 Type 2
- Priority support
Enterprise (Custom)
- Multi-region
- PITR
- SLA 99.99%
- Dedicated support
Best Practices
Naming Conventions
# Bazy danych
my-app-production
my-app-staging
# Branche
main # Production
staging # Pre-production
feature-auth # Feature branch
hotfix-bug-123 # Hotfix branchMigration Workflow
# 1. Local development
pscale connect my-app dev --port 3306
npx prisma db push
# 2. Create feature branch
pscale branch create my-app feature-xyz
# 3. Test on branch
pscale connect my-app feature-xyz --port 3306
# Run tests...
# 4. Create deploy request
pscale deploy-request create my-app feature-xyz
# 5. Review and deploy
pscale deploy-request deploy my-app 1
# 6. Cleanup
pscale branch delete my-app feature-xyzSecurity
# Rotuj hasła regularnie
pscale password delete my-app main old-password
pscale password create my-app main new-password
# Używaj różnych credentials per environment
# production-creds, staging-creds, dev-creds
# Service tokens dla CI/CD
pscale service-token createFAQ - Najczęściej Zadawane Pytania
Czy mogę używać foreign keys?
Nie natywnie. PlanetScale używa Vitess, który nie wspiera FK constraints. Używaj relationMode = "prisma" w Prisma lub waliduj w aplikacji.
Jak migrować z MySQL RDS?
- Export z RDS:
mysqldump - Stwórz bazę w PlanetScale
- Import:
pscale database import - Zaktualizuj connection string
- Usuń FK constraints z schematu
Czy PlanetScale wspiera transactions?
Tak, ale z ograniczeniami Vitess. Single-shard transactions działają normalnie. Cross-shard wymagają aplikacyjnej koordynacji.
Jak debugować wolne zapytania?
- Sprawdź Insights w dashboard
- Użyj EXPLAIN na problematycznych queries
- Dodaj sugerowane indeksy
- Monitoruj Query Insights
Czy mogę hostować PlanetScale self-hosted?
Nie. PlanetScale to managed service. Dla self-hosted użyj Vitess bezpośrednio.
Jaka jest różnica między branch a database?
Database to cała baza (jak repo). Branch to izolowana kopia schematu (jak git branch). Branche współdzielą dane z parent branch do momentu rozgałęzienia.
Podsumowanie
PlanetScale rewolucjonizuje zarządzanie bazami MySQL:
- Database branching - workflow jak w Git
- Zero-downtime migrations - bez przestojów
- Serverless - automatyczne skalowanie
- Developer experience - nowoczesne CLI i dashboard
- Enterprise-ready - Vitess pod spodem
Idealny dla zespołów chcących traktować bazę danych jak kod, z pełnym CI/CD workflow i bez obawy o downtime podczas migracji.
PlanetScale - a complete guide to serverless MySQL
What is PlanetScale?
PlanetScale is a serverless MySQL database built on Vitess - the same technology that powers YouTube and handles billions of queries per day. PlanetScale introduces a revolutionary approach to database management through branching (like Git), zero-downtime schema changes, and automatic scaling.
Traditional databases require manual schema management, planned downtime for migrations, and complex infrastructure for scaling. PlanetScale eliminates these problems by offering a developer experience familiar from modern tools like GitHub - you create a branch, make changes, open a deploy request, and merge without any downtime.
Why PlanetScale?
Key advantages of PlanetScale
- Database Branching - Isolated environments for development and testing
- Zero-downtime schema changes - Migrations without production downtime
- Automatic scaling - From hobby to enterprise without configuration
- Vitess under the hood - Battle-tested YouTube technology
- Serverless - Pay for usage, not for idle time
- Edge-ready - Low latencies globally
- Schema insights - AI-powered recommendations
- Connection pooling - Built-in PlanetScale Proxy
PlanetScale vs traditional MySQL
| Feature | PlanetScale | MySQL RDS | MySQL self-hosted |
|---|---|---|---|
| Branching | Yes | No | No |
| Schema changes | Zero downtime | Downtime | Downtime |
| Scaling | Automatic | Manual | Manual |
| Replicas | Automatic | Manual | Manual |
| Foreign keys | Prisma relations | Native | Native |
| Connection pooling | Built-in | Additional | Additional |
| Backups | Automatic | Manual | Manual |
| Pricing | Usage-based | Instance-based | Hardware |
PlanetScale vs other serverless databases
| Feature | PlanetScale | Neon | Supabase | CockroachDB |
|---|---|---|---|---|
| Engine | MySQL/Vitess | PostgreSQL | PostgreSQL | Custom |
| Branching | Yes | Yes | No | No |
| Free tier | 5GB | 3GB | 500MB | 5GB |
| Edge | Yes | Yes | No | Yes |
| Schema changes | Zero downtime | Downtime | Downtime | Zero downtime |
When to choose PlanetScale?
- You need MySQL - Not PostgreSQL
- Frequent schema changes - CI/CD for your database
- Global application - Edge locations
- Team workflow - Branching and review
- Zero-downtime - Critical production environments
Installation and configuration
PlanetScale CLI
# macOS
brew install planetscale/tap/pscale
# Linux
curl -sSL https://github.com/planetscale/cli/releases/latest/download/pscale_linux_amd64.tar.gz | tar xz
sudo mv pscale /usr/local/bin/
# Windows (scoop)
scoop bucket add pscale https://github.com/planetscale/scoop-bucket.git
scoop install pscale
# Verification
pscale versionLogging in and creating a database
# Log in
pscale auth login
# Create a new database
pscale database create my-app --region us-east
# List available regions
pscale region list
# List databases
pscale database listAvailable regions
| Region | Code | Location |
|---|---|---|
| US East | us-east | N. Virginia |
| US West | us-west | Oregon |
| EU West | eu-west | Dublin |
| Asia Pacific | ap-south | Mumbai |
| Asia Pacific | ap-northeast | Tokyo |
| Australia | ap-southeast | Sydney |
| South America | sa-east | São Paulo |
Database branching
The branching concept
PlanetScale treats your database like code - you can create isolated branches to work on new features without affecting production:
# Create a development branch
pscale branch create my-app feature-auth
# List branches
pscale branch list my-app
# Show branch details
pscale branch show my-app feature-auth
# Delete a branch
pscale branch delete my-app feature-authBranch workflow
# 1. Create a branch for the feature
pscale branch create my-app add-user-profile
# 2. Connect to the branch locally
pscale connect my-app add-user-profile --port 3309
# 3. Apply schema changes
mysql -h 127.0.0.1 -P 3309 -u root < migrations/add_profile.sql
# 4. Create a deploy request (like a PR)
pscale deploy-request create my-app add-user-profile
# 5. Review the changes
pscale deploy-request diff my-app 1
# 6. Deploy to main (merge)
pscale deploy-request deploy my-app 1Branch types
# Development branch - for daily work
pscale branch create my-app dev
# Staging branch - for testing
pscale branch create my-app staging
# Production branch - main (default)
# Protected, requires a deploy request
# Feature branch - short-lived
pscale branch create my-app feature-xyz --from stagingSafe migrations
PlanetScale automatically detects dangerous migrations:
-- ✅ Safe - adding a column
ALTER TABLE users ADD COLUMN bio TEXT;
-- ✅ Safe - adding an index
CREATE INDEX idx_users_email ON users(email);
-- ⚠️ Requires attention - changing column type
ALTER TABLE users MODIFY COLUMN age BIGINT;
-- ❌ Dangerous - dropping a column with data
ALTER TABLE users DROP COLUMN legacy_field;Zero-downtime schema changes
How does it work?
PlanetScale uses Online Schema Change (OSC) inspired by gh-ost:
- Shadow table - Creates a copy of the table with the new schema
- Data copy - Copies data in the background
- Changelog - Tracks changes during copying
- Atomic swap - Switches tables atomically
- Cleanup - Removes the old table
-- Your DDL
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(500);
-- PlanetScale executes:
-- 1. CREATE TABLE _users_new LIKE users;
-- 2. ALTER TABLE _users_new ADD COLUMN avatar_url...
-- 3. INSERT INTO _users_new SELECT * FROM users (chunked)
-- 4. RENAME TABLE users TO _users_old, _users_new TO users;
-- 5. DROP TABLE _users_old;
-- Zero downtime for your application!Deploy requests
# Create a deploy request
pscale deploy-request create my-app feature-auth
# Show diff
pscale deploy-request diff my-app 1
# Output:
# -- users table
# + avatar_url VARCHAR(500)
# + bio TEXT
# -- INDEX changes
# + idx_users_email
# Add a comment
pscale deploy-request comment my-app 1 "Reviewed, LGTM"
# Deploy
pscale deploy-request deploy my-app 1
# Track progress
pscale deploy-request show my-app 1Schema across environments
# Compare branches
pscale branch diff my-app main feature-auth
# Promote staging to production
pscale deploy-request create my-app staging --into main
# Revert (create a new deploy request with reversed changes)
pscale deploy-request revert my-app 1Connecting to your application
Connection strings
# Generate a connection string
pscale password create my-app main production-password
# Output:
# HOST: aws.connect.psdb.cloud
# USERNAME: xxxxx
# PASSWORD: pscale_pw_xxxxx
# DATABASE: my-app
# Connection string format
# mysql://USERNAME:PASSWORD@HOST/DATABASE?ssl={"rejectUnauthorized":true}Local connection (development)
# Open a tunnel to the branch
pscale connect my-app main --port 3306
# Now you can connect locally
mysql -h 127.0.0.1 -P 3306 -u root
# Or from your application
DATABASE_URL="mysql://root@127.0.0.1:3306/my-app"Prisma ORM
npm install prisma @prisma/client
npx prisma init// prisma/schema.prisma
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma" // Required for PlanetScale!
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
}
model Post {
id String @id @default(cuid())
title String
content String? @db.Text
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
}# .env
DATABASE_URL='mysql://xxxxx:pscale_pw_xxxxx@aws.connect.psdb.cloud/my-app?sslaccept=strict'# Push schema to PlanetScale
npx prisma db push
# Generate client
npx prisma generate// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}// Usage
import { prisma } from '@/lib/prisma'
// Create
const user = await prisma.user.create({
data: {
email: 'user@example.com',
name: 'John Doe'
}
})
// Read with relations
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: true }
})
// Update
await prisma.user.update({
where: { id: user.id },
data: { name: 'Jane Doe' }
})
// Delete
await prisma.post.delete({
where: { id: postId }
})Drizzle ORM
npm install drizzle-orm @planetscale/database
npm install -D drizzle-kit// db/schema.ts
import { mysqlTable, varchar, text, boolean, timestamp, index } from 'drizzle-orm/mysql-core'
export const users = mysqlTable('users', {
id: varchar('id', { length: 128 }).primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 255 }),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
}, (table) => ({
emailIdx: index('email_idx').on(table.email)
}))
export const posts = mysqlTable('posts', {
id: varchar('id', { length: 128 }).primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
content: text('content'),
published: boolean('published').default(false),
authorId: varchar('author_id', { length: 128 }).notNull(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
}, (table) => ({
authorIdx: index('author_idx').on(table.authorId)
}))// db/index.ts
import { drizzle } from 'drizzle-orm/planetscale-serverless'
import { connect } from '@planetscale/database'
import * as schema from './schema'
const connection = connect({
host: process.env.DATABASE_HOST,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD
})
export const db = drizzle(connection, { schema })// Usage
import { db } from '@/db'
import { users, posts } from '@/db/schema'
import { eq, and } from 'drizzle-orm'
// Create
const [user] = await db.insert(users).values({
id: crypto.randomUUID(),
email: 'user@example.com',
name: 'John'
}).$returningId()
// Read
const allUsers = await db.select().from(users)
// Read with conditions
const publishedPosts = await db
.select()
.from(posts)
.where(eq(posts.published, true))
// Join
const postsWithAuthors = await db
.select({
post: posts,
author: users
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
// Update
await db
.update(users)
.set({ name: 'Jane' })
.where(eq(users.id, userId))
// Delete
await db.delete(posts).where(eq(posts.id, postId))PlanetScale serverless driver
npm install @planetscale/database// Direct driver usage
import { connect } from '@planetscale/database'
const config = {
host: process.env.DATABASE_HOST,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD
}
const conn = connect(config)
// Simple query
const results = await conn.execute('SELECT * FROM users WHERE id = ?', [userId])
console.log(results.rows)
// Insert
await conn.execute(
'INSERT INTO users (id, email, name) VALUES (?, ?, ?)',
[crypto.randomUUID(), 'user@example.com', 'John']
)
// Transaction (pseudo - PlanetScale does not support native transactions)
// Use Prisma or Drizzle for transaction-like behavior
// Batch queries
const [users, posts] = await Promise.all([
conn.execute('SELECT * FROM users'),
conn.execute('SELECT * FROM posts WHERE published = 1')
])Foreign keys in PlanetScale
Why no native foreign keys?
PlanetScale uses Vitess, which does not support foreign key constraints for better scalability. Instead, you should use:
- Application-level validation - Validate in your code
- Prisma relationMode - Emulated relations
- Database triggers (limited support)
Prisma relations
// schema.prisma
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma" // Emulates FK at the application level
}
model User {
id String @id
posts Post[]
}
model Post {
id String @id
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
@@index([authorId]) // IMPORTANT: Add the index manually!
}Manual validation
// Without ORM - manual validation
async function createPost(authorId: string, title: string) {
// Check if the author exists
const author = await conn.execute(
'SELECT id FROM users WHERE id = ?',
[authorId]
)
if (author.rows.length === 0) {
throw new Error('Author not found')
}
// Now create the post
await conn.execute(
'INSERT INTO posts (id, author_id, title) VALUES (?, ?, ?)',
[crypto.randomUUID(), authorId, title]
)
}Insights and analytics
Query insights
The PlanetScale dashboard shows:
- Slow queries - Queries above the threshold
- Query patterns - Most frequent queries
- Index suggestions - Index recommendations
- Connection stats - Active connections
# CLI insights
pscale database show my-app --web
# Opens the dashboard in your browserSchema recommendations
-- PlanetScale suggests:
-- ⚠️ Missing index detected
-- Query: SELECT * FROM posts WHERE author_id = ?
-- Recommendation:
CREATE INDEX idx_posts_author_id ON posts(author_id);
-- ⚠️ Unused index detected
-- Index: idx_legacy_field
-- Recommendation:
DROP INDEX idx_legacy_field ON users;Performance schema
-- Enable in the dashboard or:
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;Backup and recovery
Automatic backups
# List backups
pscale backup list my-app
# Restore to a new branch
pscale backup restore my-app backup-id --branch restored-data
# Export backup
pscale backup download my-app backup-idPoint-in-time recovery
# Restore to a specific moment (Enterprise)
pscale backup restore my-app \
--timestamp "2024-01-15T10:30:00Z" \
--branch pitr-restoreManual export
# Export via mysqldump
pscale connect my-app main --port 3306 &
mysqldump -h 127.0.0.1 -P 3306 -u root my-app > backup.sql
# Import
mysql -h 127.0.0.1 -P 3306 -u root my-app < backup.sqlIntegrations
Vercel
# Install the integration from the Vercel Dashboard
# Or manually:
vercel env add DATABASE_URL production
vercel env add DATABASE_URL previewNext.js API route
// app/api/users/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'
export async function GET() {
const users = await prisma.user.findMany({
take: 10,
orderBy: { createdAt: 'desc' }
})
return NextResponse.json(users)
}
export async function POST(request: Request) {
const data = await request.json()
const user = await prisma.user.create({
data: {
email: data.email,
name: data.name
}
})
return NextResponse.json(user, { status: 201 })
}Edge functions
// app/api/edge/route.ts
import { connect } from '@planetscale/database'
export const runtime = 'edge'
const conn = connect({
host: process.env.DATABASE_HOST!,
username: process.env.DATABASE_USERNAME!,
password: process.env.DATABASE_PASSWORD!
})
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
const result = await conn.execute(
'SELECT * FROM users WHERE id = ?',
[id]
)
return Response.json(result.rows[0])
}GitHub Actions CI/CD
# .github/workflows/db-migrate.yml
name: Database Migration
on:
push:
branches: [main]
paths: ['prisma/schema.prisma']
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: planetscale/setup-pscale-action@v1
with:
version: latest
- name: Authenticate
run: pscale auth login --service-token ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
- name: Create branch
run: pscale branch create ${{ secrets.PLANETSCALE_DB }} pr-${{ github.run_id }}
- name: Push schema
run: |
npx prisma db push --accept-data-loss
env:
DATABASE_URL: ${{ secrets.DATABASE_URL_BRANCH }}
- name: Create deploy request
run: |
pscale deploy-request create ${{ secrets.PLANETSCALE_DB }} pr-${{ github.run_id }} \
--org ${{ secrets.PLANETSCALE_ORG }}Scaling and performance
Read replicas
# PlanetScale automatically routes:
# - Writes -> Primary
# - Reads -> Replicas (when enabled)
# Enable read replicas in the dashboard
# Settings -> Read Replicas -> EnableConnection pooling
// PlanetScale Proxy automatically pools connections
// For serverless applications this is critical!
// Without pooling: each request = new connection
// With PlanetScale: connections are reused
const config = {
host: process.env.DATABASE_HOST, // aws.connect.psdb.cloud
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
// Proxy automatically manages the pool
}Query optimization
-- Use EXPLAIN
EXPLAIN SELECT * FROM posts WHERE author_id = 'xxx';
-- Add indexes
CREATE INDEX idx_posts_author_published
ON posts(author_id, published);
-- Avoid SELECT *
SELECT id, title, created_at FROM posts WHERE author_id = ?;
-- Use LIMIT
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20;
-- Batch operations
INSERT INTO posts (id, title, author_id)
VALUES
(?, ?, ?),
(?, ?, ?),
(?, ?, ?);Pricing
Hobby (free)
- Storage: 5GB
- Row reads: 1 billion/month
- Row writes: 10 million/month
- Branches: 1 production + 1 dev
- Backups: Daily
Scaler ($29/month)
- Storage: 10GB (+ $2.50/GB)
- Row reads: 100 billion/month
- Row writes: 50 million/month
- Branches: Unlimited
- Backups: Hourly
- Read replicas: 1 region
Team ($39/user/month)
- Everything from Scaler
- Audit logs
- SSO/SAML
- SOC 2 Type 2
- Priority support
Enterprise (custom)
- Multi-region
- PITR
- SLA 99.99%
- Dedicated support
Best practices
Naming conventions
# Databases
my-app-production
my-app-staging
# Branches
main # Production
staging # Pre-production
feature-auth # Feature branch
hotfix-bug-123 # Hotfix branchMigration workflow
# 1. Local development
pscale connect my-app dev --port 3306
npx prisma db push
# 2. Create feature branch
pscale branch create my-app feature-xyz
# 3. Test on branch
pscale connect my-app feature-xyz --port 3306
# Run tests...
# 4. Create deploy request
pscale deploy-request create my-app feature-xyz
# 5. Review and deploy
pscale deploy-request deploy my-app 1
# 6. Cleanup
pscale branch delete my-app feature-xyzSecurity
# Rotate passwords regularly
pscale password delete my-app main old-password
pscale password create my-app main new-password
# Use different credentials per environment
# production-creds, staging-creds, dev-creds
# Service tokens for CI/CD
pscale service-token createFAQ - frequently asked questions
Can I use foreign keys?
Not natively. PlanetScale uses Vitess, which does not support FK constraints. Use relationMode = "prisma" in Prisma or validate in your application.
How do I migrate from MySQL RDS?
- Export from RDS:
mysqldump - Create a database in PlanetScale
- Import:
pscale database import - Update the connection string
- Remove FK constraints from the schema
Does PlanetScale support transactions?
Yes, but with Vitess limitations. Single-shard transactions work normally. Cross-shard transactions require application-level coordination.
How do I debug slow queries?
- Check Insights in the dashboard
- Use EXPLAIN on problematic queries
- Add suggested indexes
- Monitor Query Insights
Can I self-host PlanetScale?
No. PlanetScale is a managed service. For self-hosted, use Vitess directly.
What is the difference between a branch and a database?
A database is the entire database (like a repo). A branch is an isolated copy of the schema (like a git branch). Branches share data with the parent branch up until the point of divergence.
Summary
PlanetScale revolutionizes MySQL database management:
- Database branching - Git-like workflow
- Zero-downtime migrations - No downtime
- Serverless - Automatic scaling
- Developer experience - Modern CLI and dashboard
- Enterprise-ready - Vitess under the hood
Ideal for teams that want to treat their database like code, with a full CI/CD workflow and no fear of downtime during migrations.