1---
2title: Postgres
3description: PlanetScale Postgres as Stack resources — databases, branches, roles with least-privilege inherited roles, and direct vs pooled connection origins.
4---
5
6PlanetScale Postgres is managed PostgreSQL with database branching. In
7alchemy the database, its branches, and the roles that connect to them
8are resources in the same Stack as your compute — a branch forks per
9preview stage and tears down with it, and a role's credentials flow
10into whatever consumes them (Cloudflare Hyperdrive, a Worker, a
11Lambda) as typed outputs.
12
13New here? [Set up credentials](/planetscale/setup) first. For the
14MySQL (Vitess) family, see [MySQL](/planetscale/data/mysql).
15
16## Create a database
17
18`PostgresDatabase` owns the long-lived cluster. `clusterSize` is
19required.
20
21Network-attached (NAS) sizes like `"PS_10"` are expanded to the full
22SKU using the target region and architecture:
23
24```typescript
25import * as Planetscale from "alchemy/Planetscale";
26
27const database = yield* Planetscale.PostgresDatabase("app-db", {
28 region: { slug: "us-east" },
29 clusterSize: "PS_10",
30});
31```
32
33[PlanetScale Metal](https://planetscale.com/docs/metal) is supported
34too. Metal uses locally-attached NVMe, so the API needs the **full
35SKU** — CPU series, compute size, provider, architecture, and drive
36size — not a short `"M-10"` name. Copy a SKU from
37[Postgres pricing](https://planetscale.com/docs/postgres/pricing) or
38your org's `list_cluster_size_skus` response; hyphens from the docs
39are normalized to underscores:
40
41```typescript
42const metal = yield* Planetscale.PostgresDatabase("app-db-metal", {
43 region: { slug: "us-east" },
44 clusterSize: "M1_10_AWS_ARM_D_METAL_10",
45 arch: "arm",
46});
47```
48
49No name required — alchemy generates a unique one from the app,
50stage, and logical ID. Pass `name` to control it.
51
52Changing `clusterSize` resizes the cluster in place through
53PlanetScale's change-request API (alchemy queues the change and waits
54for it to complete). Changing `region`, `replicas`, or `arch`
55replaces the database.
56
57## Branch the database
58
59`PostgresBranch` is a cheap fork. Pass the database resource (or a
60plain string name) and, optionally, a parent branch:
61
62```typescript
63const branch = yield* Planetscale.PostgresBranch("app-branch", {
64 database,
65 parentBranch: "main",
66});
67```
68
69`parentBranch` defaults to `"main"` and also accepts another
70`PostgresBranch`, so branches can chain. Branches take the same
71`migrations` / `importFiles` props as the database — see
72[Migrations](/planetscale/data/migrations) for how SQL files are hashed
73and applied, and the
74[branch from a shared database](/cloudflare/data/branch-from-shared-database)
75guide for the branch-per-PR pattern.
76
77## Roles
78
79`PostgresRole` materializes a Postgres user + password on a branch.
80Permissions come from `inheritedRoles` — built-in roles like
81`postgres` or the `pg_*` family:
82
83```typescript
84const role = yield* Planetscale.PostgresRole("app-role", {
85 database,
86 branch,
87 inheritedRoles: ["postgres"],
88});
89```
90
91Scope roles down to what each consumer needs:
92
93```typescript
94const reader = yield* Planetscale.PostgresRole("reader", {
95 database,
96 branch,
97 inheritedRoles: ["pg_read_all_data", "pg_read_all_settings"],
98});
99```
100
101Pass `ttl` (seconds) for short-lived credentials. PlanetScale returns
102the password exactly once at create time, so almost every prop change
103— `ttl`, `database`, `branch`, `inheritedRoles` — replaces the role
104with a fresh one whose new credentials propagate to downstream
105resources automatically. Only the role's `name` and its `successor`
106(the role that inherits ownership when this one is dropped) update in
107place.
108
109## The default role
110
111Every branch comes with a built-in default role. `PostgresDefaultRole`
112captures it as a resource:
113
114```typescript
115const defaultRole = yield* Planetscale.PostgresDefaultRole("main-role", {
116 database,
117 forceReset: true,
118});
119```
120
121Two caveats, both consequences of the password being returned only at
122create time:
123
124- **No adoption.** If the branch's default role already exists and
125 your Stack has no record of it, the deploy fails rather than
126 silently taking over. Opt in with `forceReset: true`, which resets
127 the role — same role, **new password** — invalidating any existing
128 credentials.
129- **Destroy resets, not deletes.** There is no delete endpoint for
130 the default role, so destroying the resource resets it to
131 invalidate the credentials your Stack was holding.
132
133Prefer `PostgresRole` for application access; reach for the default
134role when a tool specifically needs it.
135
136## `origin` vs `pooledOrigin`
137
138Both role resources expose parsed connection components ready to feed
139into consumers like [Cloudflare Hyperdrive](/cloudflare/data/hyperdrive):
140
141- `role.origin` — the **direct** connection (port 5432). Use it where
142 the consumer does its own pooling, e.g. as a Hyperdrive origin.
143- `role.pooledOrigin` — the **pooled** connection via PSBouncer
144 (port 6432). Use it where each request would otherwise open a fresh
145 direct connection, e.g. Hyperdrive's local-dev origin.
146
147```typescript
148import * as Cloudflare from "alchemy/Cloudflare";
149
150const hyperdrive = yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", {
151 origin: role.origin, // direct — Hyperdrive pools at the edge
152 dev: role.pooledOrigin, // local dev bypasses Hyperdrive, so pool at PlanetScale
153});
154```
155
156If you need a URL instead of components, `connectionUrl` and
157`connectionUrlPooled` carry the same direct/pooled endpoints as
158`Redacted` connection strings (`sslmode=verify-full`).
159
160## Connect privately with AWS PrivateLink
161
162Postgres roles expose the branch's provider-neutral private connection
163details. For AWS PrivateLink, pass `privateConnectionServiceName` to
164an Interface VPC endpoint. With private DNS enabled, use
165`privateHost` for workloads in that VPC:
166
167```typescript
168import * as AWS from "alchemy/AWS";
169
170const endpoint = yield* AWS.EC2.VpcEndpoint("postgres-private-link", {
171 vpcId: vpc.vpcId,
172 serviceName: role.privateConnectionServiceName,
173 vpcEndpointType: "Interface",
174 subnetIds: [privateSubnet.subnetId],
175 securityGroupIds: [databaseSecurityGroup.groupId],
176 privateDnsEnabled: true,
177});
178```
179
180For GCP Private Service Connect, `privateHost` is the private DNS zone;
181prepend the endpoint name configured in GCP before connecting.
182`PostgresDefaultRole` exposes the same details, though `PostgresRole`
183remains the safer choice for applications.
184
185## Where next
186
187- [Migrations](/planetscale/data/migrations) — `migrations` and
188 `importFiles` on databases and branches.
189- [Hyperdrive](/cloudflare/data/hyperdrive) — edge-pooled connections from
190 Workers to your PlanetScale database.
191- [Drizzle guide](/cloudflare/data/drizzle) — generate migration SQL
192 from a Drizzle schema and apply it on deploy.
193
194Reference:
195
196- [PostgresDatabase](/providers/planetscale/postgres/postgresdatabase) ·
197 [PostgresBranch](/providers/planetscale/postgres/postgresbranch) ·
198 [PostgresRole](/providers/planetscale/postgres/postgresrole) ·
199 [PostgresDefaultRole](/providers/planetscale/postgres/postgresdefaultrole)