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.
Postgres roles
Section titled “Postgres roles”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.
The default role
Section titled “The default role”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.
MySQL passwords
Section titled “MySQL passwords”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.
TTLs and CIDR allowlists
Section titled “TTLs and CIDR allowlists”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.
origin vs pooledOrigin
Section titled “origin vs pooledOrigin”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.
Where next
Section titled “Where next”- 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: