Skip to content

Drizzle Postgres on Railway

Run Drizzle in a Railway container beside Railway Postgres. Start with Railway 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 here belongs to your migration runner; the deployed Service receives its own private connection through a binding.

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 rather than generating migrations during deployment.

src/db.ts
import * as Railway from "alchemy/Railway";
import { Site } from "./project.ts";
export const Db = Railway.Postgres("Db", { project: Site });

Use the same Project and environment as the Service; omitting environment on both selects the Project’s primary environment.

src/api.ts
{
project: Site,
main: import.meta.url,
build: { install: ["pg"] },
}

Use Railway.Service, whose image can install pg, rather than assuming the single-file Canvas Railway.Function has the same packaging support.

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

The binding injects the private URI into the Service environment; conn.connectionString is an Effect yielding a redacted URL, not a query client. These excerpts use the tutorial’s minimal text handler until the database is migrated.

Terminal window
pnpm alchemy deploy

Keep yield* Api in the tutorial’s Stack with Railway.providers(). This initial deployment provisions Postgres while the handler still avoids application tables.

Terminal window
pnpm exec drizzle-kit migrate

For an external CI runner, supply the database’s publicConnectionUri as secret DATABASE_URL; the default public: true creates that TCP proxy. With public: false, run migrations inside the same Railway environment using connectionUri instead.

The generated Railway Postgres URLs currently include sslmode=no-verify; a public TCP proxy does not provide certificate verification. For production, configure a verifiable server certificate and migration-client trust, or run migrations within the private environment.

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

Runtime traffic stays on Railway’s private network, regardless of whether a public migration endpoint exists.

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

The request opens the pool lazily and closes it when its scope ends.

Terminal window
pnpm alchemy deploy

For later changes, generate, review, and commit the schema, SQL, and snapshots before running migrations and deploying compatible code. Railway.Postgres has no migrations prop, so deployment does not replace drizzle-kit migrate.

See the SQL database comparison, Postgres client guide, and migration workflow. The railway-service example contains the Service, pg installation, and Postgres binding used here.