Next.js + Vercel + Supabaseでデータベースシード時の接続エラー完全解決ガイド
1. はじめに
📚 この記事の経緯
Next.jsの公式チュートリアル「Learn Next.js」の第6章「Setting Up Your Database」を進めている時に発生した接続エラーと、その解決過程を記録した記事です。
🎯 対象読者
- Next.jsのチュートリアルを進めている初心者
- Supabaseでデータベース接続エラーに遭遇した方
-
CONNECTION_CLOSED
エラーで困っている方
💡 記事の価値
- 教科書通りに進めてもエラーが起きる実例
- 具体的なエラーメッセージと解決手順
- Supabaseの接続方式の理解
2. 環境構成
使用技術
- Next.js: 15.3.2 (Turbopack)
- ホスティング: Vercel
- データベース: Supabase (PostgreSQL)
- パッケージマネージャー: pnpm
プロジェクト構成
nextjs-dashboard/
├── app/
│ ├── seed/
│ │ └── route.js # シードスクリプト
│ └── lib/
│ └── placeholder-data.ts # サンプルデータ
├── .env # 環境変数
└── package.json
3. シードスクリプトとは
🌱 シード(seed)とは
データベースの「種まき」という意味で、初期データを投入する処理のことです。
Next.jsチュートリアルでの役割:
- 空のデータベースにサンプルデータを投入
- ユーザー、顧客、請求書、売上データを作成
- アプリケーションがすぐに動作確認できる状態にする
実際の処理内容:
// app/seed/route.ts の主な処理
import bcrypt from 'bcrypt';
import postgres from 'postgres';
import { invoices, customers, revenue, users } from '../lib/placeholder-data';
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
async function seedUsers() {
await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
await sql`
CREATE TABLE IF NOT EXISTS users (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
);
`;
const insertedUsers = await Promise.all(
users.map(async (user) => {
const hashedPassword = await bcrypt.hash(user.password, 10);
return sql`
INSERT INTO users (id, name, email, password)
VALUES (${user.id}, ${user.name}, ${user.email}, ${hashedPassword})
ON CONFLICT (id) DO NOTHING;
`;
}),
);
return insertedUsers;
}
なぜ時間がかかるのか:
- 複数のテーブル作成(users, customers, invoices, revenue)
- bcryptでのパスワードハッシュ化(セキュリティのため)
- 大量のINSERT文の実行
4. 発生した問題
4-1. CONNECTION_CLOSEDエラー
症状
-
localhost:3000/seed
にアクセス - 1分以上Pending状態が続く
- 最終的にエラーが返される
エラーメッセージ
{"error":{"code":"CONNECTION_CLOSED","errno":"CONNECTION_CLOSED","address":["aws-0-us-east-1.pooler.supabase.com"],"port":[6543]}}
原因
Supabaseの接続プール(6543番ポート)による長時間処理の強制切断
5. 解決策: 接続方式の変更
5-1. Supabaseの接続方式の理解
🔌 Supabaseへの2つの接続方法
Supabaseには2つの入り口があります:
接続プール(共有入り口)
あなたのアプリ → 共有受付 → Supabaseデータベース
(6543番)
仕組み:
- 共有の受付窓口を通してデータベースにアクセス
- 複数のアプリが同じ窓口を使い回し
- 窓口が「効率化のために」接続を管理
メリット:
- 速い(すでに準備された接続を使える)
- 効率的(Supabase側の負荷が少ない)
デメリット:
- 時間制限あり:長時間使うと「他の人が待ってるから」と切断される
- 制限が厳しい:無料プランでは特に厳格
直接接続(専用入り口)
あなたのアプリ ──────→ Supabaseデータベース
(5432番)
仕組み:
- 専用の入り口から直接データベースにアクセス
- 他のアプリの影響を受けない
- 時間制限が緩い
メリット:
- 安定:途中で切断されにくい
- 長時間処理対応:シードなどの重い処理も大丈夫
デメリット:
- 少し遅い(毎回新しい接続を作成)
- Supabase側の負荷が高い
🚨 なぜシード処理で問題になったか
シードスクリプトの処理:
- テーブルを作成(10秒)
- ユーザーのパスワードをハッシュ化(30秒)← 時間かかる
- データを挿入(20秒)
接続プール(6543番)の場合:
- 受付窓口が「1分以上は長すぎる!他の人が待ってる!」
-
強制的に接続を切断 →
CONNECTION_CLOSED
エラー
直接接続(5432番)の場合:
- 専用入り口なので時間制限が緩い
- 最後まで処理完了 → 成功
5-2. 具体的な解決手順
# Step 1: 現在の設定削除
sed -i '' '/^POSTGRES_URL=/d' .env
# Step 2: NON_POOLING接続を使用
grep POSTGRES_URL_NON_POOLING .env | sed 's/POSTGRES_URL_NON_POOLING=/POSTGRES_URL=/' >> .env
# Step 3: 開発サーバー再起動
pnpm run dev
5-3. 成功結果
{"message":"Database seeded successfully"}
6. エラーの詳細解説
6-1. CONNECTION_CLOSED - Supabase接続タイムアウト
🔍 なぜこのエラーが起きるのか
Supabaseの接続プール(6543番ポート)には制限があります:
- ⏰ 時間制限: 長時間の処理は途中で切断される
- 👥 共有制限: 他のユーザーと共有しているため優先度が低い
- 💰 無料プラン制限: 特に厳しい制限がかかる
実際に起きたこと:
- シードスクリプトがSupabaseに接続(6543番ポート)✅
- テーブル作成開始 ✅
- bcryptでパスワードのハッシュ化(時間がかかる処理)⏳
- Supabase側が「この接続は長すぎる」と判断
-
強制的に接続を切断 →
CONNECTION_CLOSED
エラー
🎯 解決方法
- 6543番(接続プール)→ 5432番(直接接続)に変更
- 直接接続なら時間制限が緩い
7. ベストプラクティス
7-1. 接続設定の選択指針
🎯 Supabaseでの接続方法の使い分け
処理タイプ | 使用ポート | 理由 |
---|---|---|
通常のWebアプリ | 6543番(接続プール) | 速い、効率的 |
シード・データ移行 | 5432番(直接接続) | 長時間処理対応 |
📊 Vercelが提供する環境変数の使い分け
# 通常のアプリケーション用(速い)
POSTGRES_URL="...pooler.supabase.com:6543..."
# シード・長時間処理用(安定)
POSTGRES_URL_NON_POOLING="...pooler.supabase.com:5432..."
🔄 実際の切り替え方法
# シード時は NON_POOLING を使用
sed -i '' '/^POSTGRES_URL=/d' .env
grep POSTGRES_URL_NON_POOLING .env | sed 's/POSTGRES_URL_NON_POOLING=/POSTGRES_URL=/' >> .env
7-2. トラブルシューティングの順序
- 環境変数の確認(POSTGRES_URLの設定)
- 接続方式の変更(プール接続から直接接続へ)
- 段階的な変更でテスト