| 1 | --- |
| 2 | title: Postgres |
| 3 | description: PlanetScale Postgres as Stack resources — databases, branches, roles with least-privilege inherited roles, and direct vs pooled connection origins. |
| 4 | --- |
| 5 | |
| 6 | PlanetScale Postgres is managed PostgreSQL with database branching. In |
| 7 | alchemy the database, its branches, and the roles that connect to them |
| 8 | are resources in the same Stack as your compute — a branch forks per |
| 9 | preview stage and tears down with it, and a role's credentials flow |
| 10 | into whatever consumes them (Cloudflare Hyperdrive, a Worker, a |
| 11 | Lambda) as typed outputs. |
| 12 | |
| 13 | New here? [Set up credentials](/planetscale/setup) first. For the |
| 14 | MySQL (Vitess) family, see [MySQL](/planetscale/data/mysql). |
| 15 | |
| 16 | ## Create a database |
| 17 | |
| 18 | `PostgresDatabase` owns the long-lived cluster. `clusterSize` is |
| 19 | required. |
| 20 | |
| 21 | Network-attached (NAS) sizes like `"PS_10"` are expanded to the full |
| 22 | SKU using the target region and architecture: |
| 23 | |
| 24 | ```typescript |
| 25 | import * as Planetscale from "alchemy/Planetscale"; |
| 26 | |
| 27 | const 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 |
| 34 | too. Metal uses locally-attached NVMe, so the API needs the **full |
| 35 | SKU** — CPU series, compute size, provider, architecture, and drive |
| 36 | size — not a short `"M-10"` name. Copy a SKU from |
| 37 | [Postgres pricing](https://planetscale.com/docs/postgres/pricing) or |
| 38 | your org's `list_cluster_size_skus` response; hyphens from the docs |
| 39 | are normalized to underscores: |
| 40 | |
| 41 | ```typescript |
| 42 | const 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 | |
| 49 | No name required — alchemy generates a unique one from the app, |
| 50 | stage, and logical ID. Pass `name` to control it. |
| 51 | |
| 52 | Changing `clusterSize` resizes the cluster in place through |
| 53 | PlanetScale's change-request API (alchemy queues the change and waits |
| 54 | for it to complete). Changing `region`, `replicas`, or `arch` |
| 55 | replaces the database. |
| 56 | |
| 57 | ## Branch the database |
| 58 | |
| 59 | `PostgresBranch` is a cheap fork. Pass the database resource (or a |
| 60 | plain string name) and, optionally, a parent branch: |
| 61 | |
| 62 | ```typescript |
| 63 | const 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 |
| 73 | and applied, and the |
| 74 | [branch from a shared database](/cloudflare/data/branch-from-shared-database) |
| 75 | guide for the branch-per-PR pattern. |
| 76 | |
| 77 | ## Roles |
| 78 | |
| 79 | `PostgresRole` materializes a Postgres user + password on a branch. |
| 80 | Permissions come from `inheritedRoles` — built-in roles like |
| 81 | `postgres` or the `pg_*` family: |
| 82 | |
| 83 | ```typescript |
| 84 | const role = yield* Planetscale.PostgresRole("app-role", { |
| 85 | database, |
| 86 | branch, |
| 87 | inheritedRoles: ["postgres"], |
| 88 | }); |
| 89 | ``` |
| 90 | |
| 91 | Scope roles down to what each consumer needs: |
| 92 | |
| 93 | ```typescript |
| 94 | const reader = yield* Planetscale.PostgresRole("reader", { |
| 95 | database, |
| 96 | branch, |
| 97 | inheritedRoles: ["pg_read_all_data", "pg_read_all_settings"], |
| 98 | }); |
| 99 | ``` |
| 100 | |
| 101 | Pass `ttl` (seconds) for short-lived credentials. PlanetScale returns |
| 102 | the password exactly once at create time, so almost every prop change |
| 103 | — `ttl`, `database`, `branch`, `inheritedRoles` — replaces the role |
| 104 | with a fresh one whose new credentials propagate to downstream |
| 105 | resources automatically. Only the role's `name` and its `successor` |
| 106 | (the role that inherits ownership when this one is dropped) update in |
| 107 | place. |
| 108 | |
| 109 | ## The default role |
| 110 | |
| 111 | Every branch comes with a built-in default role. `PostgresDefaultRole` |
| 112 | captures it as a resource: |
| 113 | |
| 114 | ```typescript |
| 115 | const defaultRole = yield* Planetscale.PostgresDefaultRole("main-role", { |
| 116 | database, |
| 117 | forceReset: true, |
| 118 | }); |
| 119 | ``` |
| 120 | |
| 121 | Two caveats, both consequences of the password being returned only at |
| 122 | create 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 | |
| 133 | Prefer `PostgresRole` for application access; reach for the default |
| 134 | role when a tool specifically needs it. |
| 135 | |
| 136 | ## `origin` vs `pooledOrigin` |
| 137 | |
| 138 | Both role resources expose parsed connection components ready to feed |
| 139 | into 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 |
| 148 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 149 | |
| 150 | const 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 | |
| 156 | If 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 | |
| 162 | Postgres roles expose the branch's provider-neutral private connection |
| 163 | details. For AWS PrivateLink, pass `privateConnectionServiceName` to |
| 164 | an Interface VPC endpoint. With private DNS enabled, use |
| 165 | `privateHost` for workloads in that VPC: |
| 166 | |
| 167 | ```typescript |
| 168 | import * as AWS from "alchemy/AWS"; |
| 169 | |
| 170 | const 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 | |
| 180 | For GCP Private Service Connect, `privateHost` is the private DNS zone; |
| 181 | prepend the endpoint name configured in GCP before connecting. |
| 182 | `PostgresDefaultRole` exposes the same details, though `PostgresRole` |
| 183 | remains 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 | |
| 194 | Reference: |
| 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) |