Skip to content

Drizzle Postgres on Fly

Run Drizzle in a Fly.Service, with Fly Managed Postgres on the same organization’s private network. Start with Fly setup, the Service tutorial, and a src/schema.ts exporting Users from the Postgres client guide.

Terminal window
pnpm add drizzle-orm@1.0.0-rc.5-ab785fc @effect/sql-pg pg
pnpm add -D drizzle-kit@1.0.0-rc.5-ab785fc

The Drizzle versions match the repository’s Effect integration.

drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "postgresql",
schema: "./src/schema.ts",
out: "./drizzle",
dbCredentials: { url: process.env.DATABASE_URL! },
});

DATABASE_URL is the direct Postgres URL used by the migration runner, not the pooled runtime URL.

Terminal window
pnpm exec drizzle-kit generate

Review the generated SQL and snapshots, including any rename decisions, before applying them.

Terminal window
git add src/schema.ts drizzle.config.ts drizzle
git diff --cached
Terminal window
git commit -m "Add users migration"

Commit the schema, SQL, and snapshots together; CI deploys this reviewed history rather than generating it.

src/db.ts
import * as Fly from "alchemy/Fly";
export const Db = Fly.Postgres("Db", { region: "iad" });

This provisions a billed Managed Postgres cluster, not an unmanaged Postgres Machine.

// src/api.ts — the Service from the tutorial
{
app: Site,
main: import.meta.url,
region: "iad",
build: { install: ["pg"] },
}

pg must be installed in the Service image because bundling its CommonJS exports breaks the driver’s interop.

import { Db } from "./db.ts";
Effect.gen(function* () {
const conn = yield* Fly.ConnectPostgres(Db);
return {
fetch: Effect.succeed(HttpServerResponse.text("Hello from Fly!")),
};
}),
}).pipe(Effect.provide(Fly.ConnectPostgresHttp)),

The binding attaches the cluster to the Service’s App and supplies pooled and direct connection Effects. These excerpts use the tutorial’s minimal text handler until the database is migrated.

Terminal window
pnpm alchemy deploy

Keep the tutorial’s yield* Api and public IP in the Stack, using Fly.providers(). This first deployment creates the database and a handler that does not yet query application tables.

Terminal window
pnpm exec drizzle-kit migrate

Supply the cluster’s connectionUri as the migration runner’s secret DATABASE_URL, and run where the direct hostname resolves and is reachable on the Fly organization network, such as a correctly configured WireGuard peer or a Machine on 6PN. A pooled URI is not the migration endpoint.

import * as Drizzle from "alchemy/Drizzle/Postgres";
const conn = yield* Fly.ConnectPostgres(Db);
const db = yield* Drizzle.Postgres(conn.connectionString);

The pooled connection opens lazily when a request first queries the database.

import { Users } from "./schema.ts";
return {
fetch: Effect.succeed(HttpServerResponse.text("Hello from Fly!")),
fetch: Effect.gen(function* () {
const users = yield* db.select().from(Users).pipe(Effect.orDie);
return yield* HttpServerResponse.json({ users });
}),
};

Queries run inside the request scope, rather than during infrastructure construction.

Terminal window
pnpm alchemy deploy

For subsequent schema changes, generate, review, and commit first, then apply the committed migrations before deploying compatible application code. alchemy deploy does not replace drizzle-kit migrate in this guide.

Fly.Postgres also supports a migrations directory, but this guide deliberately leaves it unset and keeps Drizzle as the migration owner. Switching an existing database to Alchemy’s migration history is a separate migration adoption decision, not a command substitution.

See the Managed Postgres guide, portable Postgres queries, and migration workflow. The fly-postgres example demonstrates the Service binding, but its test-only HTTP migration endpoint is not used here.