How to Integrate PlanetScale with Astro: Complete Guide
Step-by-step guide to integrating PlanetScale with your Astro website. Setup, configuration, and best practices.
How to Integrate PlanetScale with Astro: Complete Guide
PlanetScale is a serverless MySQL-compatible database platform built on Vitess, the same technology that powers YouTube's database infrastructure. When paired with Astro, it gives you a production-grade database backend for dynamic pages, API routes, and server-side rendered content without managing database servers yourself.
This guide walks you through connecting PlanetScale to an Astro project from scratch, covering installation, configuration, querying data, and deploying to production. It targets Astro 6, which shipped on March 10, 2026 and changed how environment variables and output modes work compared to older guides you may have seen.
Prerequisites
Before starting, make sure you have the following ready:
- Node.js 22.12.0 or later installed on your machine (Astro 6 requires Node 22.12.0 and above, dropping the Node 18 and Node 20 support that earlier Astro versions had)
- An existing Astro project running v6.x
- A PlanetScale account (sign up at planetscale.com)
- A PlanetScale database already created through the dashboard
- Basic familiarity with SQL and JavaScript/TypeScript
Because database queries run on the server, you also need an Astro adapter so the relevant pages can render on demand. We cover that in the installation step.
Installation
Start by installing the PlanetScale serverless driver, which communicates with your database over HTTP instead of traditional TCP connections. This makes it compatible with serverless and edge environments. The current published version is @planetscale/database@1.20.1.
npm install @planetscale/database
If you prefer using an ORM layer on top, you can also install Drizzle ORM (drizzle-orm@0.45.2) or Kysely (kysely@0.29.2), but the native driver works well for most use cases.
Next, add an Astro adapter so your server-side code can execute database queries. The official Node adapter (@astrojs/node@10.1.2) is the simplest choice for a self-hosted setup:
npx astro add node
Running astro add node installs the adapter and wires it into your config automatically. Astro maintains official adapters for Node.js, Netlify, Vercel, and Cloudflare, so pick the one that matches where you deploy.
Configuration
Choosing an Output Mode
A common mistake carried over from older tutorials is setting output: 'hybrid'. That value was removed in Astro 5 and does not exist in Astro 6. The current model is simpler:
output: 'static'is the default. Your entire site is prerendered to static HTML at build time.output: 'server'renders every page on demand and requires an adapter.
You do not need to switch the whole site to server mode just to query a database. Keep the default static output and opt individual pages or endpoints into on-demand rendering by exporting prerender = false from them. This is the modern replacement for the old hybrid mode.
// astro.config.mjs
import { defineConfig, envField } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
adapter: node({
mode: 'standalone',
}),
env: {
schema: {
DATABASE_HOST: envField.string({ context: 'server', access: 'secret' }),
DATABASE_USERNAME: envField.string({ context: 'server', access: 'secret' }),
DATABASE_PASSWORD: envField.string({ context: 'server', access: 'secret' }),
},
},
});
Note there is no output key here, so the site stays static by default while the adapter remains available for any page that opts into on-demand rendering. The env.schema block is covered next.
Setting Up Environment Variables
Create or update your .env file in the project root with your PlanetScale connection credentials. You can find these in the PlanetScale dashboard under your database's "Connect" section. Choose database-js from the language and framework dropdown to get correctly formatted credentials.
DATABASE_HOST=aws.connect.psdb.cloud
DATABASE_USERNAME=your_username_here
DATABASE_PASSWORD=pscale_pw_your_password_here
An important Astro 6 change affects how you read these values. In Astro 6, import.meta.env values are always inlined at build time, so reading database credentials through import.meta.env.DATABASE_PASSWORD in server code would bake whatever was present at build time into the bundle instead of reading the live runtime value. The recommended approach is the type-safe astro:env API. You declare each variable in the env.schema block shown above with context: 'server' and access: 'secret', then import the typed values from astro:env/server. Secret server variables defined this way are never included in the client bundle.
Creating the Database Client
Create a utility file to initialize the PlanetScale connection. This keeps your database logic centralized and reusable across your project. Pull the credentials from astro:env/server rather than import.meta.env.
// src/lib/db.ts
import { Client } from '@planetscale/database';
import { DATABASE_HOST, DATABASE_USERNAME, DATABASE_PASSWORD } from 'astro:env/server';
export const db = new Client({
host: DATABASE_HOST,
username: DATABASE_USERNAME,
password: DATABASE_PASSWORD,
});
The driver exposes both a connect() helper and a Client class. The PlanetScale docs recommend the Client factory for per-request handlers because it creates a fresh connection per transaction or web request, which suits Astro's server endpoints. Either way the client is lightweight and makes HTTP requests to PlanetScale's edge network, so there is no persistent connection pool to manage. If you genuinely need a single shared connection, connect(config) returns one directly.
Common Patterns
Querying Data in Astro Pages
Use the database client inside .astro page frontmatter or API routes. Because the site defaults to static output, any page that queries the database at request time must opt into on-demand rendering with export const prerender = false. Here is an example page that fetches blog posts from a posts table:
---
// src/pages/blog.astro
export const prerender = false;
import { db } from '../lib/db';
const conn = db.connection();
const result = await conn.execute(
'SELECT id, title, slug, published_at FROM posts WHERE status = ? ORDER BY published_at DESC',
['published']
);
const posts = result.rows;
---
<html>
<body>
<h1>Blog Posts</h1>
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
</body>
</html>
When you create the client with the Client class, call db.connection() to get a connection for the current request, then run execute() on it. The execute() method returns a results object whose rows property holds your data. Always use parameterized queries (the ? placeholder syntax) to prevent SQL injection.
If you would rather have the database content prerendered into static HTML, drop the prerender = false line. The query then runs at build time and the page ships as plain static HTML, which is ideal for content that does not change between deploys.
Building API Routes
You can also create JSON API endpoints that query PlanetScale. API routes that hit the database also need prerender = false:
// src/pages/api/posts.ts
export const prerender = false;
import type { APIRoute } from 'astro';
import { db } from '../../lib/db';
export const GET: APIRoute = async () => {
const conn = db.connection();
const result = await conn.execute('SELECT id, title, slug FROM posts LIMIT 20');
return new Response(JSON.stringify(result.rows), {
headers: { 'Content-Type': 'application/json' },
});
};
Using PlanetScale Branching
PlanetScale supports database branching, which works similarly to Git branches. Create a development branch for schema changes, then merge it into production through a deploy request. Use separate connection credentials for each branch in your .env.development and .env.production files:
# .env.development
DATABASE_HOST=aws.connect.psdb.cloud
DATABASE_USERNAME=dev_branch_user
DATABASE_PASSWORD=pscale_pw_dev_password
This prevents accidental schema changes on your production database.
Troubleshooting
Connection errors with "fetch is not defined": The PlanetScale driver uses the Fetch API internally. Make sure you are running Node.js 18 or later, which includes a global fetch implementation. If you are stuck on an older runtime, the driver lets you pass a custom fetch in the config, and the docs suggest undici (currently undici@8.3.0) for newer Node versions or fetch-h2 for older ones. Astro 6 itself requires at least Node 22.12.0, which ships global fetch, so this rarely bites you in practice.
Queries return empty results: Double-check that your connection credentials point to the correct database branch. PlanetScale defaults to the main branch, but your data might be on a development branch.
Common Errors and Fixes
output: 'hybrid' is not a valid value: This option was removed in Astro 5 and does not exist in Astro 6. Delete the output: 'hybrid' line entirely. Leave the default static output in place and add export const prerender = false to any page or endpoint that needs to query the database on demand. Use output: 'server' only if you want the entire site rendered on demand.
Credentials read as the build-time value instead of the runtime value: In Astro 6, import.meta.env values are always inlined at build time. If you read import.meta.env.DATABASE_PASSWORD in server code, the value present during the build gets baked into the bundle instead of the live runtime value. Define the variables in the env.schema block and import them from astro:env/server, or fall back to process.env for runtime reads. Variables marked access: 'secret' are also kept out of the client bundle.
Cannot find module 'astro:env/server' or types not resolving: The astro:env virtual module is generated from the env.schema you declare in astro.config.mjs. Run astro sync (or start the dev server, which syncs automatically) after editing the schema so the types and module are generated. A variable that is not declared in the schema will not be importable from astro:env/server; use getSecret('NAME') for variables that live outside the schema.
No adapter found for on-demand pages: Adding prerender = false without an installed adapter produces a build error telling you an adapter is required. Run npx astro add node (or your platform adapter) so on-demand pages have a server runtime to render into.
Slow query performance: PlanetScale queries go through HTTP, which adds slight latency compared to TCP connections. For pages with multiple queries, run them in parallel using Promise.all rather than sequentially. Also add proper indexes to your tables through the PlanetScale dashboard.
Type safety concerns: The raw driver returns untyped rows. If you want full TypeScript type safety, consider layering Drizzle ORM or Kysely on top of the PlanetScale driver. Both support PlanetScale as a connection target and provide typed query builders.
Official Docs and Examples
- Astro on-demand rendering and adapters: https://docs.astro.build/en/guides/on-demand-rendering/
- Astro environment variables and the
astro:envAPI: https://docs.astro.build/en/guides/environment-variables/ - Astro
astro:envmodule reference (getSecret, typed imports): https://docs.astro.build/en/reference/modules/astro-env/ - Astro Node adapter (
@astrojs/node): https://docs.astro.build/en/guides/integrations-guide/node/ - PlanetScale serverless driver source and quickstart: https://github.com/planetscale/database-js
- PlanetScale serverless driver tutorial: https://planetscale.com/docs/tutorials/planetscale-serverless-driver
- Drizzle ORM PlanetScale guide: https://orm.drizzle.team/docs/get-started-mysql#planetscale
- Kysely PlanetScale dialect: https://github.com/depot/kysely-planetscale
Conclusion
PlanetScale pairs well with Astro for projects that need a reliable MySQL database without the overhead of managing servers or connection pools. The HTTP-based driver works seamlessly in serverless and edge environments, and features like database branching give you safe schema migration workflows.
Start with the native driver for simple projects. As your application grows, add an ORM layer for type safety and query building. By keeping Astro on its default static output and opting only data-driven pages into on-demand rendering with prerender = false, you keep your marketing pages static and fast while powering dynamic features with PlanetScale on the server side.
Sources
All versions and facts below were checked on 2026-05-29.
- @planetscale/database 1.20.1, latest: https://registry.npmjs.org/@planetscale/database/latest
- @astrojs/node 10.1.2, latest: https://registry.npmjs.org/@astrojs/node/latest
- astro 6.4.2, latest: https://registry.npmjs.org/astro/latest
- drizzle-orm 0.45.2, latest: https://registry.npmjs.org/drizzle-orm/latest
- kysely 0.29.2, latest: https://registry.npmjs.org/kysely/latest
- undici 8.3.0, latest: https://registry.npmjs.org/undici/latest
- Astro output modes (static default, server, per-page prerender): https://docs.astro.build/en/guides/on-demand-rendering/
- Astro 6 inlines import.meta.env at build time, use astro:env for runtime secrets: https://docs.astro.build/en/guides/environment-variables/
- astro:env/server and getSecret reference: https://docs.astro.build/en/reference/modules/astro-env/
- PlanetScale driver Client vs connect, execute().rows, custom fetch: https://github.com/planetscale/database-js
- PlanetScale dashboard Connect and database-js credentials: https://planetscale.com/docs/tutorials/planetscale-serverless-driver
- Astro 6 release date March 10, 2026 and Node 22 baseline: https://www.netlify.com/changelog/2026-03-10-astro-6/
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.