読了 約 8 分 Cosoado Lab

Supabase pooler URL を migration/runtime で正しく使い分ける方法

migration は DIRECT_URL (port 5432) / runtime は DATABASE_URL (port 6543 Transaction mode) を使い分ける。Drizzle は prepare: false 必須。2025 年 2 月以降 Session mode は廃止。Prisma v6 / v7 / Drizzle 各 ORM の設定実例。

TL;DR

なぜ接続先を分けないといけないのか

NetaPair のリリース直前、Vercel にデプロイした本番環境で prisma migrate deploy を走らせたところ、コマンドが無音で止まった。エラーメッセージゼロ。Ctrl+C するまで 20 分間フリーズしていた。

原因を探って判明したのは単純なことで、.envDATABASE_URLpooler の URL(port 6543)を設定したまま migration を実行していただけだった。先に読んでいれば 1 分で防げた話なのに、リリース当日の夜に 1 時間溶かした。

Supabase の接続には大きく 2 種類あり、どちらを何に使うかを混同するとこういう目に遭う。

Transaction mode と direct connection の違い

Supabase の接続プーラーは現在 Supavisor(Elixir 製)が担っている。旧来の PgBouncer からは 2023 年頃に移行済みで、ダッシュボードに表示される接続文字列もすでに Supavisor ベースになっている。

Transaction poolerDirect connection
ホストaws-0-[region].pooler.supabase.comdb.[project-ref].supabase.co
ポート65435432
接続の持ち方トランザクション単位で共有セッション全体で専有
prepared statements非対応対応
SET session_replication_role非対応対応
migration禁止推奨
Vercel などサーバーレス推奨非推奨(接続数爆発)

Transaction mode は、1 トランザクション分だけバックエンド接続を借りてすぐ返却する。Vercel の関数インスタンスが大量に立ち上がっても接続数が爆発しないのはこの仕組みがあるから。一方で、セッション状態を必要とする処理が一切できない。

prisma migrate deploy や Supabase CLI の db push は内部で SET session_replication_role = 'replica' を発行するため、Transaction mode のプーラー越しでは動かない。コマンドがハングする理由はここにある。

2025 年 2 月以降のポート整理

以前は port 6543 で Session mode を使うオプションもあったが、Supabase の changelog によると 2025 年 2 月 28 日に廃止された(GitHub ディスカッション #32755 でも告知済み)。

現在のポートの役割は明確だ:

古い設定が残っている場合(port=6543 で migration を走らせているなど)は今すぐ修正したい。

環境変数の設定

Supabase ダッシュボードの Project Settings → Database に行くと「Connection string」セクションに接続文字列が複数種類表示されている。Transaction(port 6543)と Direct connection(port 5432)の 2 つを取得する。

# .env.local
# ↓ Supabase ダッシュボード → Project Settings → Database → "Transaction" タブからコピー
DATABASE_URL="<Transaction pooler URL (port 6543)>"

# ↓ Supabase ダッシュボード → Project Settings → Database → "Direct connection" タブからコピー
DIRECT_URL="<Direct connection URL (port 5432)>"

Transaction pooler の URL はホスト aws-0-[region].pooler.supabase.com、ポート 6543。Direct connection は db.[project-ref].supabase.co、ポート 5432 という構成になっている。[project-ref] は Supabase のプロジェクト ID、[region]ap-northeast-1 などの AWS リージョン。

Prisma での設定

Prisma v6 以前

schema.prismadatasource ブロックに directUrl を追加する。CLI はこちらを使い、PrismaClienturl を使う。Prisma 公式の Supabase ガイド(v6) に詳細がある。

// schema.prisma
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")   // pooler (port 6543) — runtime
  directUrl = env("DIRECT_URL")     // direct (port 5432) — migration
}

prisma migrate deployprisma db push は自動的に directUrl を参照してくれる。

Prisma v7 以降

Prisma v7.2.0 以降、schema.prisma から URL を直接指定する方法が廃止された(Discussion #28700)。代わりに prisma.config.ts で migration 時の接続を定義し、runtime 用は PrismaPg コンストラクタで指定する。

// prisma.config.ts — CLI が参照する (migration 用)
import { defineConfig } from 'prisma/config'

export default defineConfig({
  earlyAccess: true,
  schema: './prisma/schema.prisma',
  migrate: {
    async adapter() {
      const { PrismaPg } = await import('@prisma/adapter-pg')
      // DIRECT_URL を使う — direct connection (port 5432)
      return new PrismaPg({ connectionString: process.env.DIRECT_URL! })
    },
  },
})
// src/lib/db.ts — アプリが参照する (runtime 用)
import { PrismaClient } from '@prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'

// DATABASE_URL を使う — pooler (port 6543)
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! })
export const prisma = new PrismaClient({ adapter })

Drizzle ORM での設定

Drizzle は drizzle.config.ts(migration 用)と runtime のクライアント初期化で URL を分けるだけ。Drizzle 公式の Supabase チュートリアル にも記載がある。

// drizzle.config.ts — drizzle-kit push/migrate が参照 (migration 用)
import { defineConfig } from 'drizzle-kit'

export default defineConfig({
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DIRECT_URL!,  // direct connection (port 5432)
  },
})
// src/lib/db.ts — API ルートが参照 (runtime 用)
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from '@/db/schema'

// prepare: false は Transaction mode では必須
const client = postgres(process.env.DATABASE_URL!, { prepare: false })
export const db = drizzle(client, { schema })

落とし穴 2 点

prepare: false を忘れると本番だけ落ちる

これも自分でやった失敗だ。Drizzle + Transaction mode の組み合わせで prepare: false を書かずに Vercel に上げたとき、同じ API ルートへの 2 回目のリクエストから ERROR: prepared statement "xxx" already exists が出て 500 エラーになった。

ローカル(通常の PostgreSQL 直接接続)では問題なく動くので、本番にデプロイするまで全く気づかない。Vercel のログを見てエラーの原因を調べるまで「コードのバグかな」と 30 分ほど無駄にした。Transaction mode は named prepared statement をセッション間で共有できないため、prepare: false でステートレスなクエリに落とす必要がある。

migration コマンドがハングする

冒頭に書いた通り、DATABASE_URL(port 6543)を directUrl の代わりに使うと prisma migrate deploy が無音でフリーズする。タイムアウトエラーも出ない。CI/CD パイプラインに組み込んでいると、Vercel のビルドが延々と続いてタイムアウト扱いになるだけなので原因特定が余計に難しい。

.envDIRECT_URL を追加して schema.prisma(または prisma.config.ts)に正しく設定すれば解消する。

まとめ

用途環境変数ホストポート
migration (prisma/drizzle-kit)DIRECT_URLdb.[ref].supabase.co5432
runtime (API ルート)DATABASE_URL...pooler.supabase.com6543

2025 年 2 月以降、port 6543 は Transaction mode 専用になっているので、古い設定が残っていれば今すぐ確認したい。Drizzle は prepare: false を忘れずに。

参照リンク

関連プロダクト

他のプロダクト by Cosoado Lab

TsuriMate 釣り仲間・船割りメンバーマッチング YUMELIA 夢占い・数秘術が無料でできる AI 占い Web アプリ OshiVista 推し活を 1 つにまとめる管理アプリ(多言語対応) 謎かけメーカー AI と「その心は?」を競うなぞかけ・大喜利