PlanetScale

Connect Strapi v5 to PlanetScale's serverless MySQL platform for scalable, branch-based database management. This guide covers configuration, SSL setup, connection pooling, troubleshooting, and a practical blog project example

PlanetScale

What Is PlanetScale?

PlanetScale is a fully managed, MySQL-compatible serverless database platform built on Vitess, the open-source database clustering system originally developed at YouTube. It runs on locally-attached NVMe SSDs and uses 3-node clusters by default consisting of one primary and two replica nodes, with automated failovers.

Three capabilities set it apart from standard managed MySQL:

  • Database branching: Create isolated development copies of your database using Git-like branches, with development branches copying schema only (not data unless using Data Branching®) and supporting safe testing before promoting to production.
  • Non-blocking schema changes: ALTER TABLE operations execute without locking tables using Vitess's online schema change techniques and tablet throttler, so your application keeps reading and writing during migrations.
  • Horizontal sharding: Scale reads and writes across multiple MySQL instances by explicitly defining sharding keys (Primary Vindexes) through Vitess, with VTGate nodes abstracting query routing. Note: Requires application awareness of sharding strategy and manual configuration of sharding keys.

Important limitation: PlanetScale's Vitess architecture does not support database-level foreign key constraints by default, requiring application-level referential integrity management for Strapi content relationships.

Why Integrate PlanetScale with Strapi

Strapi v5 gives you a flexible, open-source headless CMS with full control over your content model, API layer, and admin panel. PlanetScale handles the database layer so you don't have to. Here's what the combination offers:

  • Zero-downtime schema migrations: PlanetScale's non-blocking schema changes allow you to modify Strapi content structures without locking database tables. Schema changes are applied gradually while your API remains fully responsive, allowing content type modifications in production without downtime.
  • Branch-based schema testing: Create a PlanetScale development branch, connect a Strapi instance to it, and test schema changes for content model modifications in complete isolation before deploying to production through PlanetScale's deploy request system.
  • Serverless-friendly connection handling: PlanetScale manages connection pooling at the platform level. The @planetscale/database driver uses HTTP/Fetch API architecture, eliminating manual pool tuning that traditional MySQL deployments demand—especially valuable when deploying Strapi to containerized or serverless environments.
  • Query performance monitoring: PlanetScale's Insights dashboard surfaces query execution time, slow queries, and connection wait times, which helps when debugging the JOIN-heavy SQL that Strapi generates for nested content relationships.
  • Schema version control that complements Git workflows: PlanetScale's deploy request system provides reviewable schema diffs, giving your team the same review process for database changes used for code.
  • Horizontal scaling for content-heavy applications: As your Strapi content grows, PlanetScale's Vitess-based architecture enables horizontal scaling through explicit sharding configuration without requiring manual database resharding.

How to Integrate PlanetScale with Strapi

Prerequisites

Before starting, confirm you have the following:

Step 1: Create a Strapi v5 Project

If you already have a Strapi v5 project, skip to Step 2. Otherwise, scaffold a new one:

npx create-strapi@latest my-planetscale-app

During the interactive setup, select your preferred language (JavaScript or TypeScript). You can choose the default SQLite database for now since you'll switch to PlanetScale's MySQL in the next steps.

cd my-planetscale-app

Step 2: Create a PlanetScale Database

Log in to the PlanetScale CLI and create a database:

pscale auth login
pscale database create my-strapi-db --region us-east

The --region flag sets your database's primary region. Pick one close to your application servers for lower latency.

Next, generate connection credentials for the main branch:

pscale password create my-strapi-db main production-credentials

This outputs connection details you need for the next step:

According to PlanetScale's connection string documentation, connection credentials are formatted as:

Host: aws.connect.psdb.cloud
Username: [auto-generated-username]
Password: [generated-password]
Database: [your-database-name]

Or as a complete connection string:

mysql://[USERNAME]:[PASSWORD]@aws.connect.psdb.cloud/[DATABASE_NAME]?ssl={"rejectUnauthorized":true}

Credentials are created per database branch through the PlanetScale dashboard or CLI using pscale password create <DATABASE_NAME> <BRANCH_NAME> <PASSWORD_NAME>.

Save these values. You won't be able to retrieve the password again after this.

Step 3: Install the MySQL Client

Strapi v5 requires the mysql client library for database connections. You'll need the MySQL client library in your project:

npm install mysql

Step 4: Configure Environment Variables

Store your PlanetScale credentials in the .env file at your project root:

DATABASE_HOST=aws.connect.psdb.cloud
DATABASE_PORT=3306
DATABASE_NAME=my-strapi-db
DATABASE_USERNAME=your-planetscale-username
DATABASE_PASSWORD=your-planetscale-password
DATABASE_SSL=true
DATABASE_SSL_REJECT_UNAUTHORIZED=true

Never commit .env to version control. Verify it's listed in your .gitignore.

Step 5: Configure the Database Connection

Update config/database.js (or config/database.ts) with a PlanetScale-specific configuration:

// config/database.js
module.exports = ({ env }) => ({
  connection: {
    client: 'mysql',
    connection: {
      host: env('DATABASE_HOST'),
      port: env.int('DATABASE_PORT', 3306),
      database: env('DATABASE_NAME'),
      user: env('DATABASE_USERNAME'),
      password: env('DATABASE_PASSWORD'),
      ssl: env.bool('DATABASE_SSL', false) && {
        rejectUnauthorized: env.bool('DATABASE_SSL_REJECT_UNAUTHORIZED', true),
      },
      timezone: 'utc',
    },
    pool: {
      min: 0,
      max: 10,
      acquireTimeoutMillis: 60000,
      idleTimeoutMillis: 30000,
      createTimeoutMillis: 30000,
    },
    debug: false,
  },
  settings: {
    forceMigration: true,
    runMigrations: true,
  },
});

A few configuration choices worth noting:

  • ssl.rejectUnauthorized: true: PlanetScale requires SSL/TLS for all connections. Their certificates are signed by commonly trusted system root CAs, so you can keep verification enabled.
  • pool.min: 0: Essential for containerized and serverless deployments. Unlike Strapi's standard configuration (pool.min: 2), setting min: 0 prevents holding stale connections that trigger ECONNRESET errors when PlanetScale closes idle connections after inactivity.
  • acquireTimeoutMillis: 60000: Generous timeout that accounts for cold starts and network variability when connecting to PlanetScale's distributed infrastructure.

Step 6: Start Strapi and Verify the Connection

Start the development server:

npm run develop

When you start Strapi with a configured database connection, Strapi uses its experimental migration system to initialize the database schema. According to Strapi's database migrations documentation, Strapi runs one-time migration scripts before performing automatic schema synchronization based on your content types. If the connection and migrations succeed, you'll access the Strapi admin panel at the configured application URL.

Open the admin panel and create your first administrator account. At this point, Strapi is reading from and writing to your PlanetScale database.

To confirm directly, open a PlanetScale shell:

pscale shell my-strapi-db main
SHOW TABLES;

You should see Strapi's core tables: strapi_core_store_settings, admin_users, admin_permissions, and others.

Step 7: Handle Foreign Key Constraints

This is where PlanetScale's architecture introduces a notable constraint. PlanetScale's Vitess foundation does not enforce foreign key constraints at the database level by default due to its distributed database design. Since Strapi relies on relational content models with foreign key relationships, you have two options: either implement referential integrity at the application layer using Strapi lifecycle hooks or enable foreign key constraint support through PlanetScale's paid plans.

Option A: Enable foreign key support (available in paid PlanetScale plans, with referential integrity at application layer for free plans)

If you need database-level foreign key constraints, PlanetScale supports them on paid plans as an exception to its Vitess-based architecture. However, PlanetScale's default configuration disables foreign key constraints due to its distributed database design. Alternatively, implement referential integrity at the Strapi application layer using lifecycle hooks.

Option B: Manage integrity at the application layer

Since PlanetScale's Vitess architecture does not support foreign key constraints by default, Strapi's lifecycle hooks provide an effective way to enforce referential integrity at the application layer. Strapi already handles most relationship management internally, so for many content-driven applications, this application-level approach works well in practice when database-level foreign key constraints are unavailable.

Project Example: Travel Blog with Next.js and Strapi on PlanetScale

To see this integration in action, let's build a travel blog where Strapi manages the content, PlanetScale handles the database, and Next.js renders the frontend. This pattern works for any content-driven site: portfolios, documentation, editorial platforms, or e-commerce storefronts.

Set Up the Strapi Content Model

With Strapi connected to PlanetScale (from the integration steps above), open the admin panel and navigate to the Content-Type Builder.

Create a Blog Post Collection Type with these fields:

Field NameTypeDetails
titleTextRequired, unique
slugUIDAttached to title
contentRich TextRequired
excerptTextShort summary, 280 character limit
coverImageMediaSingle image
destinationRelationMany-to-one with Destination
publishedAtDateTimeFor scheduling

Create a Destination Collection Type:

Field NameTypeDetails
nameTextRequired, unique
slugUIDAttached to name
descriptionRich TextOverview of the destination
regionEnumerationAsia, Europe, Americas, Africa, Oceania

After saving these content types, Strapi automatically generates the corresponding MySQL tables in your PlanetScale database and exposes REST and GraphQL endpoints.

Add Sample Content

Use the Content Manager to create a few destinations and blog posts. Make sure to publish them so they're accessible through the API.

Before fetching content from your frontend, configure the Users & Permissions plugin in Strapi's admin panel. Navigate to Settings → Users & Permissions → Roles → Public, and enable find and findOne permissions for your Blog Post and Destination content types.

Fetch Content from Next.js

In your Next.js project, create a utility for fetching from Strapi's REST API:

// lib/strapi.js
const STRAPI_URL = process.env.NEXT_PUBLIC_STRAPI_URL || 'http://localhost:1337';

export async function fetchAPI(path, params = {}) {
  const queryString = new URLSearchParams(params).toString();
  const url = `${STRAPI_URL}/api${path}${queryString ? `?${queryString}` : ''}`;

  const response = await fetch(url, {
    headers: { 'Content-Type': 'application/json' },
    next: { revalidate: 60 },
  });

  if (!response.ok) {
    throw new Error(`Strapi API error: ${response.status}`);
  }

  return response.json();
}

Build the blog listing page:

// app/blog/page.js
import { fetchAPI } from '@/lib/strapi';
import Link from 'next/link';

export default async function BlogPage() {
  const { data: posts } = await fetchAPI('/blog-posts', {
    'populate': 'destination,coverImage',
    'sort': 'publishedAt:desc',
  });

  return (
    <main>
      <h1>Travel Blog</h1>
      <div>
        {posts.map((post) => (
          <article key={post.id}>
            <Link href={`/blog/${post.slug}`}>
              <h2>{post.title}</h2>
            </Link>
            <p>{post.excerpt}</p>
            {post.destination && <span>{post.destination.name}</span>}
          </article>
        ))}
      </div>
    </main>
  );
}

Fetch individual posts with the filters API:

// app/blog/[slug]/page.js
import { fetchAPI } from '@/lib/strapi';

export default async function BlogPost({ params }) {
  const { data: posts } = await fetchAPI('/blog-posts', {
    'filters[slug][$eq]': params.slug,
    'populate': '*',
  });

  const post = posts[0];

  if (!post) return <div>Post not found</div>;

  return (
    <article>
      <h1>{post.title}</h1>
      {post.destination && <p>Destination: {post.destination.name}</p>}
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Use PlanetScale Branching for Content Model Changes

Here's where PlanetScale's branching really pays off. Suppose you need to add a travelTips Dynamic Zone to your blog posts. Instead of modifying production directly, you can create a development branch, test your schema changes in complete isolation, and then deploy them to production through a deploy request with zero downtime.

According to PlanetScale's non-blocking schema changes, these alterations are applied gradually without locking tables, so your blog remains fully accessible to readers and editors throughout the deployment.

Create an isolated development branch:

pscale branch create my-strapi-db add-travel-tips

Update your local .env to point at the development branch:

DATABASE_HOST=127.0.0.1
DATABASE_PORT=3306
DATABASE_SSL=false

Then open a local tunnel to the development branch:

pscale connect my-strapi-db add-travel-tips --port 3306

Start Strapi and make your content type changes through the admin panel. Strapi will automatically apply the schema changes to your isolated PlanetScale development branch.

For production deployments with safe migrations enabled, this workflow requires additional coordination: extract the schema changes Strapi applies, create a deploy request in PlanetScale containing those changes, review the deploy request to verify compatibility with non-blocking migrations, and deploy through PlanetScale's deployment system before promoting your Strapi application code.

Test everything on the development branch first, then follow the proper deployment workflow for production changes.

Once you're satisfied with the changes, create and deploy a deploy request:

pscale deploy-request create my-strapi-db add-travel-tips

Review the diff in the PlanetScale dashboard, then deploy:

pscale deploy-request deploy my-strapi-db 1

Deploy your updated Strapi code alongside the schema change. PlanetScale's non-blocking schema changes enable deployment without downtime, and your production API continues processing requests throughout the migration.

Production Database Configuration

For production deployment, tighten your configuration:

// config/database.js
module.exports = ({ env }) => ({
  connection: {
    client: 'mysql',
    connection: {
      host: env('DATABASE_HOST'),
      port: env.int('DATABASE_PORT', 3306),
      database: env('DATABASE_NAME'),
      user: env('DATABASE_USERNAME'),
      password: env('DATABASE_PASSWORD'),
      ssl: {
        rejectUnauthorized: true,
      },
      timezone: 'utc',
    },
    pool: {
      min: 0,
      max: 10,
      acquireTimeoutMillis: 60000,
      idleTimeoutMillis: 30000,
      createTimeoutMillis: 30000,
    },
    debug: false,
  },
  settings: {
    forceMigration: true,
    runMigrations: true,
  },
});

Use separate PlanetScale passwords for each environment (development, staging, production). According to PlanetScale's connection documentation, you can create multiple credentials per branch, eliminating any need to share passwords across environments.

Strapi Open Office Hours

If you have any questions about Strapi 5 or want to join us, visit Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST (18:30–19:30 UTC): Strapi Discord Open Office Hours.

For more details, visit the Strapi documentation and PlanetScale documentation.

Frequently Answered Questions

PlanetScale's plan availability and feature set vary, so check their current pricing page for the latest details. Strapi v5 requires MySQL 8.0+ compatibility, which PlanetScale provides across all tiers. A key consideration is that PlanetScale does not support database-level foreign key constraints by default. Since Strapi creates relational content models, you must manage referential integrity at the application layer through Strapi's lifecycle hooks or enable foreign key support through PlanetScale's paid plan options.

This error occurs because Strapi's MySQL package doesn't support MySQL 8's default caching_sha2_password authentication protocol, a documented issue in the Strapi repository (GitHub Issue #13774). The recommended fix is changing your PlanetScale user's authentication method to mysql_native_password: ALTER USER 'username'@'%' IDENTIFIED WITH mysql_native_password BY 'password'. Note that Strapi only supports the mysql client dialect—using mysql2 results in an error.

PlanetScale's default configuration does not support foreign key constraints due to Vitess's distributed database design, which complicates query routing across shards. Some paid plans allow enabling foreign key support through database settings. Without database-level enforcement, Strapi still works because it manages relationship integrity through its ORM. You can add validation using Strapi's lifecycle hooks to verify referential integrity before operations reach the database, preventing orphaned records at the application level.

When safe migrations are active on a production branch, direct DDL statements are blocked—but Strapi auto-synchronizes schemas on startup. To resolve this, connect Strapi to a PlanetScale development branch during content type changes, letting automatic synchronization run freely on the isolated branch. Then create a deploy request to review and promote those changes to production. Deploy your updated Strapi application code after the schema migration completes, ensuring careful coordination between both systems.

PlanetScale closes idle connections after approximately 15 minutes. If Strapi's Knex connection pool maintains persistent connections (the default pool.min: 2), those connections go stale and trigger ECONNRESET or KnexTimeoutError on the next query. Set pool.min: 0 in your config/database.js so the pool can drain completely during idle periods. Also configure idleTimeoutMillis: 30000, createTimeoutMillis: 30000, and acquireTimeoutMillis: 60000 to handle connection recycling gracefully.