PlanetScale Security Guide for Vibe Coders
Published on January 23, 2026 - 10 min read
TL;DR
PlanetScale is a serverless MySQL platform with Git-like branching. Keep connection strings in environment variables, use branch passwords with minimal permissions, never deploy schema changes directly to production (use deploy requests), and implement query parameterization in your ORM. Their branching model is powerful but can expose data if development branches have production data.
Why PlanetScale Security Matters for Vibe Coding
PlanetScale's serverless MySQL platform with database branching is incredibly developer-friendly. But when AI tools generate database code, they often miss important security patterns. The branching model, while powerful, creates unique risks around credential management and data exposure across branches.
Common issues we see in AI-generated PlanetScale code include hardcoded connection strings, missing parameterized queries, and development branches containing sensitive production data.
Connection String Security
PlanetScale connection strings contain credentials and should never be committed to code.
The Secure Setup
# .env (never commit this file)
DATABASE_URL="mysql://username:password@aws.connect.psdb.cloud/database?sslaccept=strict"
# For branch-specific connections
DATABASE_URL_MAIN="mysql://..."
DATABASE_URL_DEV="mysql://..."
// Using with Prisma (recommended ORM)
// schema.prisma
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma"
}
Common AI-Generated Mistake
AI tools sometimes inline the connection string directly in the code or configuration. Always use environment variables. Also, PlanetScale requires sslaccept=strict for secure connections.
Branch Password Strategy
Create separate passwords for each branch with appropriate permissions:
# Via PlanetScale CLI
# Production branch - read-only for most services
pscale password create my-db main read-only --ttl 90d
# Production branch - full access for migrations only
pscale password create my-db main admin --ttl 7d
# Development branch - full access for devs
pscale password create my-db dev admin --ttl 30d
Branch Workflow Security
PlanetScale's branching allows you to make schema changes safely, but improper use can expose data.
Safe Branch Creation
# Create a development branch from production
pscale branch create my-db feature-xyz --from main
# This creates schema-only copy by default (safe)
# If you need data, be careful about sensitive info
Deploy Request Workflow
Never push schema changes directly to production. Always use deploy requests:
# Make changes on development branch
pscale connect my-db feature-xyz
# Create deploy request (like a PR for your schema)
pscale deploy-request create my-db feature-xyz
# Review the changes
pscale deploy-request diff my-db 1
# Deploy after review
pscale deploy-request deploy my-db 1
Query Security
SQL injection is still possible with PlanetScale. Use parameterized queries.
With Prisma (Recommended)
// SAFE: Prisma parameterizes automatically
const user = await prisma.user.findUnique({
where: { email: userInput }
});
// SAFE: Even with dynamic values
const users = await prisma.user.findMany({
where: {
OR: [
{ name: { contains: searchTerm } },
{ email: { contains: searchTerm } }
]
}
});
// DANGEROUS: Raw queries need careful handling
const result = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${email}
`;
// This IS safe because Prisma parameterizes template literals
// DANGEROUS: String concatenation
const result = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE email = '${email}'`
);
// This is NOT safe - never use $queryRawUnsafe with user input
With Direct MySQL Connection
import mysql from 'mysql2/promise';
const connection = await mysql.createConnection(process.env.DATABASE_URL);
// SAFE: Parameterized query
const [rows] = await connection.execute(
'SELECT * FROM users WHERE email = ?',
[email]
);
// DANGEROUS: String concatenation
const [rows] = await connection.execute(
`SELECT * FROM users WHERE email = '${email}'`
);
Row-Level Security Patterns
PlanetScale doesn't have built-in row-level security like Postgres, so implement it at the application layer:
// Middleware to enforce tenant isolation
class SecureUserRepository {
constructor(private prisma: PrismaClient, private tenantId: string) {}
// All queries automatically scoped to tenant
async findUsers() {
return this.prisma.user.findMany({
where: { tenantId: this.tenantId }
});
}
async findUser(id: string) {
const user = await this.prisma.user.findUnique({
where: { id }
});
// Verify tenant ownership
if (user?.tenantId !== this.tenantId) {
throw new Error('Access denied');
}
return user;
}
async updateUser(id: string, data: UpdateUserData) {
// First verify ownership
await this.findUser(id);
return this.prisma.user.update({
where: { id },
data
});
}
}
Schema Migration Safety
Secure your migration process to prevent accidental data loss or exposure:
// prisma/migrations should be version controlled
// But never include sensitive seed data
// seed.ts - Development only
async function main() {
// Only seed on non-production branches
if (process.env.DATABASE_URL?.includes('main')) {
console.error('Cannot seed production database');
process.exit(1);
}
await prisma.user.create({
data: {
email: 'test@example.com',
// Use fake data, never real production data
}
});
}
Audit Logging
Implement audit logging for sensitive operations:
// Prisma middleware for audit logging
prisma.$use(async (params, next) => {
const sensitiveModels = ['User', 'Payment', 'ApiKey'];
if (sensitiveModels.includes(params.model || '')) {
const before = Date.now();
const result = await next(params);
const after = Date.now();
await prisma.auditLog.create({
data: {
model: params.model,
action: params.action,
userId: getCurrentUserId(), // From your auth context
duration: after - before,
timestamp: new Date()
}
});
return result;
}
return next(params);
});
PlanetScale Security Checklist
- Connection strings stored in environment variables
- SSL mode set to strict in connection string
- Branch passwords created with minimal required permissions
- Production passwords have short TTL and are rotated regularly
- Schema changes go through deploy requests, never direct to main
- All queries use parameterization (Prisma or prepared statements)
- No production data copied to development branches
- Row-level security implemented at application layer
- Audit logging enabled for sensitive tables
- Database passwords not shared between environments
IP Restrictions
PlanetScale allows IP allowlisting for additional security:
# In PlanetScale dashboard or via API:
# Settings > IP Restrictions
# Allow only specific IPs
# - Your production server IPs
# - Your CI/CD runner IPs
# - Developer office IPs (for dev branches only)
Backup and Recovery Security
PlanetScale handles backups automatically, but understand the security implications:
- Backups are encrypted at rest
- Point-in-time recovery available within retention window
- Restoration creates a new branch (doesn't overwrite)
- Restoring from backup requires admin permissions
Should I use the same connection string for all environments?
No. Use separate branch passwords for each environment (development, staging, production). Production should have more restrictive permissions and shorter TTLs on passwords.
::
Can I copy production data to a development branch?
Technically yes, but you should avoid it. Development branches should use synthetic or anonymized data. If you must use production data, anonymize sensitive fields first and ensure the branch has appropriate access controls.
How do I handle migrations in CI/CD?
Create a dedicated password with admin permissions and a short TTL specifically for CI/CD. Store it in your CI secrets. Use deploy requests for production changes, which can be auto-deployed after review.
Is data encrypted in PlanetScale?
Yes, PlanetScale encrypts data at rest and in transit. Connections require SSL by default. For highly sensitive data, consider application-level encryption as an additional layer.
::
What CheckYourVibe Detects
When scanning your PlanetScale-connected project, CheckYourVibe identifies:
- Hardcoded database connection strings
- Missing SSL in connection strings
- Raw SQL queries vulnerable to injection
- $queryRawUnsafe usage with user input
- Missing tenant isolation in multi-tenant apps
- Seed files containing real user data
Run npx checkyourvibe scan to catch these issues before they reach production.