Skip to content
GitHub Discord

AWS PlanetScale Drizzle Postgres

In this example, we use PlanetScale Postgres with a branch-per-stage pattern. Every stage gets its own database branch — so each PR can have an isolated database.

sst.config.ts
const db = planetscale.getDatabasePostgresOutput({
id: "mydb",
organization: "myorg",
});
const branch =
$app.stage === "production"
? planetscale.getPostgresBranchOutput({
id: db.defaultBranch,
organization: db.organization,
database: db.name,
})
: new planetscale.PostgresBranch("DatabaseBranch", {
database: db.name,
organization: db.organization,
name: $app.stage,
parentBranch: db.defaultBranch,
});

We then create a role and wrap it in a Linkable to link it to a function.

sst.config.ts
new sst.aws.Function("Api", {
handler: "src/api.handler",
link: [database],
url: true,
});

You can push your Drizzle schema changes to PlanetScale with:

Terminal window
bun run db:push

The database role sets successor to postgres so PlanetScale can reassign owned schema objects before deleting a non-production branch role.

In the function we use Drizzle ORM with the Resource helper.

src/drizzle.ts
import { drizzle } from "drizzle-orm/postgres-js";
import { Resource } from "sst";
import postgres from "postgres";
const client = postgres({
host: Resource.Database.host,
username: Resource.Database.username,
password: Resource.Database.password,
database: Resource.Database.database,
});
export const db = drizzle(client);
sst.config.ts
const db = planetscale.getDatabasePostgresOutput({
id: "mydb",
organization: "myorg",
});
const branch =
$app.stage === "production"
? planetscale.getPostgresBranchOutput({
id: db.defaultBranch,
organization: db.organization,
database: db.name,
})
: new planetscale.PostgresBranch("DatabaseBranch", {
database: db.name,
organization: db.organization,
name: $app.stage,
parentBranch: db.defaultBranch,
});
const role = new planetscale.PostgresBranchRole("DatabaseRole", {
database: db.name,
organization: db.organization,
branch: branch.name,
name: `${$app.name}-${$app.stage}`,
inheritedRoles: [
"pg_read_all_data",
"pg_write_all_data",
"postgres", // Only needed for pushing schema changes
],
successor: "postgres",
});
const database = new sst.Linkable("Database", {
properties: {
host: role.accessHostUrl,
username: role.username,
password: role.password,
database: role.databaseName,
port: 6432, // Use 5432 for direct connection instead of PgBouncer
},
});
const api = new sst.aws.Function("Api", {
handler: "src/api.handler",
link: [database],
url: true,
});
src/api.ts
import { db } from "./drizzle";
import { todosTable } from "./schema";
import { APIGatewayProxyHandlerV2 } from "aws-lambda";
export const handler: APIGatewayProxyHandlerV2 = async (evt) => {
if (evt.requestContext.http.method === "GET") {
const result = await db.select().from(todosTable).execute();
return {
statusCode: 200,
body: JSON.stringify(result),
};
}
if (evt.requestContext.http.method === "POST") {
await db.insert(todosTable).values({ title: "new todosTable" }).execute();
return {
statusCode: 200,
body: "created",
};
}
return {
statusCode: 404,
body: "not found",
};
};

View the full example.