Skip to content

Credentials

PlanetScale credentials are resources: Postgres branches use roles (PostgresRole, PostgresDefaultRole), MySQL branches use passwords (MySQLPassword). One rule shapes all three — PlanetScale returns the plaintext exactly once, at create time. Alchemy stores it Redacted in state, and any change it can’t apply in place mints a fresh credential whose new plaintext propagates to downstream resources automatically.

PostgresRole mints a Postgres user + password on a branch; permissions come from inheritedRoles:

import * as Planetscale from "alchemy/Planetscale";
const reader = yield* Planetscale.PostgresRole("reader", {
database,
branch, // defaults to "main"
inheritedRoles: ["pg_read_all_data", "pg_read_all_settings"],
});

Only name and successor (the role that inherits ownership when this one is dropped) update in place — changing ttl, inheritedRoles, database, or branch replaces the role with a new id, name, and password.

Every Postgres branch ships with a built-in default role; PostgresDefaultRole captures it as a resource:

const defaultRole = yield* Planetscale.PostgresDefaultRole("main-role", {
database,
forceReset: true,
});

The default role cannot be adopted: if it already exists and your Stack has no record of it, the deploy fails (Default role already exists ... Use forceReset: true to reset) rather than silently taking over. forceReset: true opts in by resetting it — same role, new password — which invalidates any credentials other systems were using. Destroying the resource also resets rather than deletes (PlanetScale has no delete endpoint for it). Prefer PostgresRole for application access.

MySQLPassword mints credentials scoped to one branch and one role — "reader", "writer", "admin", or "readwriter":

const password = yield* Planetscale.MySQLPassword("app-password", {
database,
branch, // defaults to "main"
role: "reader",
});

name and cidrs update in place (same id, no new plaintext); changing role, ttl, replica, database, or branch replaces the password and deletes the old one.

Both families take a ttl (seconds) for credentials that expire on their own; MySQLPassword additionally takes cidrs to restrict which IP ranges may connect:

const admin = yield* Planetscale.MySQLPassword("admin-password", {
database,
role: "admin",
ttl: 86400,
cidrs: ["203.0.113.0/24"],
});

Changing ttl is a replacement on both roles and passwords (the expiry is baked in at create time); editing cidrs on a MySQL password syncs in place on the next deploy.

Credentials expose parsed connection components ready to feed into consumers like Cloudflare Hyperdrive. PostgresRole has two: origin is the direct connection (port 5432), pooledOrigin goes through PSBouncer (port 6432):

import * as Cloudflare from "alchemy/Cloudflare";
const hyperdrive = yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", {
origin: role.origin, // direct — Hyperdrive pools at the edge
dev: role.pooledOrigin, // local dev bypasses Hyperdrive, so pool at PlanetScale
});

MySQLPassword exposes a single origin, and PostgresDefaultRole carries the same direct/pooled endpoints as Redacted URL strings (connectionUrl / connectionUrlPooled), as does PostgresRole.

  • Postgres — databases, branches, and roles in context.
  • MySQL — the Vitess side, end to end.
  • Hyperdrive — edge-pooled connections from Workers to your PlanetScale database.

Reference: