Skip to content

feat(postgres): add generic Postgres.Database resource with migration support - #1100

Open
wanjohiryan wants to merge 1 commit into
alchemy-run:mainfrom
nestrilabs:feat/postgres-resource
Open

feat(postgres): add generic Postgres.Database resource with migration support#1100
wanjohiryan wants to merge 1 commit into
alchemy-run:mainfrom
nestrilabs:feat/postgres-resource

Conversation

@wanjohiryan

Copy link
Copy Markdown

Adds a Postgres.Database resource that connects to any Postgres instance and handles
migrations automatically — the same migrationsDir / importFiles flow as Neon and
PlanetScale, but for self- or cloud-hosted Postgres.

Usage

import * as Postgres from "alchemy/Postgres";

// Connection string — simplest path
const db = yield* Postgres.Database("app-db", {
  connectionString: Secret("DATABASE_URL"),
  migrationsDir: "./migrations",
});

// Individual params
const db = yield* Postgres.Database("app-db", {
  host: "db.internal",
  port: 5432,
  user: "app",
  password: Secret("DB_PASSWORD"),
  database: "my_app",
  migrationsDir: "./migrations",
});

Dev mode — auto Docker container

In dev you never set up a local Postgres manually. alchemy dev starts one for you:

const db = yield* Postgres.Database("app-db", {
  host: "prod-db.internal",
  user: "app",
  password: Secret("DB_PASSWORD"),
  database: "my_app",
  migrationsDir: "./migrations",
  dev: {
    docker: {
      image: "postgres:18-alpine",
    },
  },
});

Data persists in .alchemy/storage/postgres/app-db/data via a Docker bind mount.

A pg_isready healthcheck ensures the container is accepting connections before
migrations run.

Dev mode — passthrough

Use a different connection in dev (e.g. a staging DB):

const db = yield* Postgres.Database("app-db", {
  connectionString: Secret("PROD_DATABASE_URL"),
  migrationsDir: "./migrations",
  dev: {
    host: "localhost",
    user: "dev_user",
    database: "dev_app",
  },
});

Wired into Hyperdrive

The origin attribute feeds directly into Cloudflare.Hyperdrive:

const db = yield* Postgres.Database("app-db", {
  connectionString: Secret("DATABASE_URL"),
});

const hd = yield* Cloudflare.Hyperdrive.Connection("app-hd", {
  origin: db.origin,
});

XOR compilation errors

Two compiler-guarded constraints:

// ❌ connectionString + host (mutually exclusive)
Database("db", { connectionString: "...", host: "..." })

// ❌ dev.docker + dev.host (mutually exclusive)
Database("db", { dev: { docker: {...}, host: "..." } })

Migration internals

  • Creates __alchemy_migrations tracking table on first run
  • Applies each .sql file inside a transaction (BEGIN → query → INSERT → COMMIT)
  • Skips already-applied files by name
  • importFiles are content-hashed and re-applied on change (no tracking table row)

What this PR does not include

  • No read / list implementation (these providers are pass-through, there's no
    cloud API to query)
  • No binding/connect layer for Workers — Hyperdrive is the intended path for now
  • No live-deploy tests (see disclosure below)

Files

packages/alchemy/src/Postgres/
  Database.ts            # Resource type, Props, XOR types, Attributes
  DatabaseProvider.ts    # ProviderLayer.dual — live (passthrough) + local (dev/docker)
  Migrations.ts          # pg-based migration runner
  PostgresOrigin.ts      # PostgresOrigin type, parse/build utilities
  Providers.ts           # ProviderCollection registration
  index.ts               # Barrel exports
packages/alchemy/package.json  # Added "./Postgres" and "./Postgres/*" exports

Disclosure

a) This feature was developed with the assistance of AI (Anthropic, Kimi & DeepSeek).
All code was manually reviewed for correctness against the existing alchemy
provider patterns.

b) The feature has not been tested thoroughly because this repository depends on
git submodules (distilled, cloudflare-tools) that recursively pull in gigabytes
of dependencies — framework-specific adapters for Astro, Svelte, Waku, Nuxt,
React Start, Next.js, etc. These submodules are unnecessary for a single new
resource and would consume excessive bandwidth and storage. A full bun install
followed by bun tsc -b could not be run. All 17 import paths were verified
manually against their target exports; one bug (incorrect namespace import for
ProviderLayer) was caught and fixed during review.

… support

Adds a Postgres.Database resource that connects to any Postgres instance
(self-hosted, cloud-hosted, or local) with built-in migration support
using the same migrationsDir / importFiles pattern as Neon and PlanetScale.

- Connection via URL string or individual host/port/user/password/database fields
- XOR types enforce compile-time mutual exclusion (connectionString vs fields, dev.docker vs dev passthrough)
- Dev mode: auto-starts a local Postgres Docker container or passthrough connection
- Migrations: creates tracking table, applies .sql files in transactions, skips already-applied
- Migrations table default: __alchemy_migrations
- PostgresOrigin output consumable by Cloudflare.Hyperdrive
- Docker volume: .alchemy/storage/postgres/{id}/data bind mount
@sam-goodwin

Copy link
Copy Markdown
Contributor

So does this deploy a database or just connect to one?

@wanjohiryan

Copy link
Copy Markdown
Author

It uses an already existing one... only in dev does it run a container (if docker option is true). It is supposed to give an e2e managed service for anyone who is trying to use alchemy as IAC, and they do not use Neon or Planetscale.

It also does migration for you on deploy... which is what i was mostly going for.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants