はじめに
モダンな Web アプリケーション開発では、プルリクエストごとにプレビュー環境を自動構築することが一般的になっています。しかし、データベースと OAuth 認証を含むプレビュー環境の構築には、いくつかの技術的な課題があります。
本記事では、以下の技術スタックでプレビューデプロイメント環境を構築する際に遭遇した問題と、その解決策を詳しく解説します:
- Vercel: ホスティング・デプロイメント
- Turso: SQLite 互換の分散データベース
- Better Auth: TypeScript 向け認証フレームワーク
- GitHub Actions: CI/CD パイプライン
目標とするアーキテクチャ
プレビュー環境の要件
- ブランチごとに独立したデータベース: 各プレビュー環境が独自のデータベースを持つ
- OAuth 認証の動作: プレビュー URL でも Google 認証が正常に動作する
- 自動化されたワークフロー: PR 作成時に自動でデプロイ、クローズ時に自動でクリーンアップ
全体フロー
問題 1: Vercel CLI デプロイで環境変数が適用されない
発生した症状
GitHub Actions から vercel deploy --prebuilt でデプロイ後、ランタイムで以下のエラーが発生:
❌ Invalid environment variables: [
{
expected: 'string',
code: 'invalid_type',
path: [ 'TURSO_DATABASE_URL' ],
message: 'Invalid input: expected string, received undefined'
}
]調査プロセス
1. Vercel ダッシュボードの確認
環境変数は正しく設定されていた:
$ vercel env ls
TURSO_DATABASE_URL Encrypted Preview (issue-27) 30m ago2. ブランチ固有の環境変数の取得テスト
$ vercel env pull --environment=preview --git-branch=issue-27 .env.local
TURSO_DATABASE_URL="libsql://linto-issue-27-xxx.turso.io"環境変数自体は正しく設定されている。
3. デプロイメントの検証
$ vercel inspect https://linto-xxx.vercel.app
target: preview
# ブランチ情報が表示されない!根本原因
Vercel の gitBranch パラメータは、Git 連携(自動デプロイ)経由のデプロイにのみ適用されることが判明しました。
{
"key": "TURSO_DATABASE_URL",
"value": "libsql://...",
"target": ["preview"],
"gitBranch": "issue-27"
}この設定は、GitHub/GitLab 連携による自動デプロイでは機能しますが、vercel deploy --prebuilt による CLI デプロイでは適用されません。
なぜこの仕様なのか
Vercel CLI デプロイは Git 連携を使用しないため:
- Git 情報の欠如: CLI デプロイには Git のブランチ情報が自動的に含まれない
--meta gitBranchの限界: メタデータとして記録されるだけで、環境変数の選択には影響しない- 設計上の分離: CLI デプロイは「ローカルビルドのアップロード」として扱われる
解決策
vercel deploy の --env オプションを使用して、デプロイ時に直接環境変数を渡す:
- name: Deploy to Vercel
run: |
vercel deploy --prebuilt \
--env TURSO_DATABASE_URL=${{ steps.db_url.outputs.url }} \
--env TURSO_AUTH_TOKEN=${{ secrets.TURSO_GROUP_TOKEN }} \
--env BETTER_AUTH_URL=$PREVIEW_URL \
--env NEXT_PUBLIC_SITE_URL=$PREVIEW_URL \
--token=${{ secrets.VERCEL_TOKEN }}各アプローチの比較
| 方法 | 動作 | CLI デプロイでの結果 |
|---|---|---|
Vercel API で gitBranch 設定 | Git 連携デプロイにのみ適用 | ❌ 適用されない |
--meta gitBranch | メタデータとして保存 | ❌ 環境変数に影響なし |
--env KEY=VALUE | デプロイ時に直接設定 | ✅ 確実に適用 |
問題 2: OAuth 認証がプレビュー環境で動作しない
発生した症状
プレビュー URL(https://project-git-issue-27.vercel.app)で Google ログインを試みると:
Error 400: redirect_uri_mismatch原因
Google OAuth は事前に登録された redirect_uri のみを許可します。プレビューデプロイメントでは URL が動的に生成されるため、すべての URL を事前に登録することは不可能です。
Better Auth の oAuthProxy プラグイン
Better Auth は、この問題を解決するための oAuthProxy プラグインを提供しています。
動作原理
設定方法
import { betterAuth } from "better-auth";
import { oAuthProxy } from "better-auth/plugins";
const PRODUCTION_URL = "https://your-app.vercel.app";
export const auth = betterAuth({
baseURL: env.BETTER_AUTH_URL,
// Vercel のプレビュードメインを信頼
trustedOrigins: ["https://*.vercel.app"],
plugins: [
oAuthProxy({
// 現在のデプロイメント URL
currentURL: env.BETTER_AUTH_URL,
// Production 環境の URL(Google に登録した redirect URI のベース)
productionURL: PRODUCTION_URL,
}),
],
socialProviders: {
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
// redirect URI を明示的に Production に設定
redirectURI: `${PRODUCTION_URL}/api/auth/callback/google`,
},
},
});重要な設定: skipStateCookieCheck
oAuthProxy を使用する場合、skipStateCookieCheck を有効にする必要があります:
export const auth = betterAuth({
account: {
skipStateCookieCheck: true,
},
// ...
});なぜ必要なのか
- プレビューサーバーで OAuth 開始: state cookie はプレビュードメインに設定される
- Google が Production にリダイレクト: callback は Production URL に送られる
- クロスドメインの cookie 問題: Production サーバーはプレビュードメインの cookie にアクセスできない
- state 検証が失敗: 通常の検証フローでは認証が失敗する
セキュリティ考慮事項
skipStateCookieCheck を有効にすることで CSRF 保護が弱まる可能性がありますが、以下の対策で軽減しています:
- trustedOrigins の制限:
https://*.vercel.appのみを信頼 - Production URL の固定: redirect_uri は常に Production URL
- HTTPS の強制: すべての通信は暗号化
カスタムドメイン使用時の設定
Vercel のデフォルトドメイン(*.vercel.app)ではなく、カスタムドメイン(例: example.com)を使用する場合は、追加の設定が必要です。
1. PRODUCTION_URL の変更
// カスタムドメインを使用する場合
const PRODUCTION_URL = "https://example.com";
// Vercel デフォルトドメインの場合
// const PRODUCTION_URL = "https://your-project.vercel.app";2. trustedOrigins の更新
カスタムドメインを使用する場合、trustedOrigins にカスタムドメインを追加する必要があります:
export const auth = betterAuth({
baseURL: env.BETTER_AUTH_URL,
// カスタムドメインと Vercel プレビュードメインの両方を信頼
trustedOrigins: [
"https://example.com", // カスタムドメイン(Production)
"https://www.example.com", // www サブドメイン(必要な場合)
"https://*.vercel.app", // Vercel プレビュードメイン
],
plugins: [
oAuthProxy({
currentURL: env.BETTER_AUTH_URL,
productionURL: "https://example.com",
}),
],
socialProviders: {
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
redirectURI: "https://example.com/api/auth/callback/google",
},
},
});3. Google Cloud Console の設定更新
カスタムドメインに変更する際は、Google Cloud Console の Authorized redirect URIs も更新が必要です:
# 変更前(Vercel デフォルトドメイン)
https://your-project.vercel.app/api/auth/callback/google
# 変更後(カスタムドメイン)
https://example.com/api/auth/callback/google注意: Google OAuth の redirect_uri は完全一致で検証されます。末尾のスラッシュの有無も区別されるため、正確に設定してください。
4. 環境別 URL の対応表
| 環境 | URL | 用途 |
|---|---|---|
| Production | https://example.com | カスタムドメイン(Google に登録) |
| Production (Vercel) | https://your-project.vercel.app | Vercel デフォルト(リダイレクト設定推奨) |
| Preview | https://your-project-git-branch.vercel.app | プレビュー環境 |
| Local | http://localhost:3000 | ローカル開発 |
5. Vercel でのリダイレクト設定
カスタムドメイン使用時、Vercel のデフォルトドメインからカスタムドメインへのリダイレクトを設定することを推奨します:
// vercel.json
{
"redirects": [
{
"source": "/(.*)",
"has": [
{
"type": "host",
"value": "your-project.vercel.app"
}
],
"destination": "https://example.com/$1",
"permanent": true
}
]
}これにより、Vercel デフォルトドメインへのアクセスがカスタムドメインにリダイレクトされ、SEO やセキュリティの観点でも一貫性が保たれます。
カスタムドメイン移行時のチェックリスト
-
PRODUCTION_URLをカスタムドメインに変更 -
trustedOriginsにカスタムドメインを追加 -
redirectURIをカスタムドメインに変更 - Google Cloud Console の redirect URI を更新
- Vercel ダッシュボードでカスタムドメインを追加
- DNS レコードを設定(CNAME または A レコード)
- SSL 証明書の自動発行を確認
- (推奨)Vercel デフォルトドメインからのリダイレクト設定
問題 3: vercel.json の設定が反映されない
発生した症状
issue-* ブランチの自動デプロイを無効化するため、以下の設定を追加:
{
"git": {
"deploymentEnabled": {
"main": true,
"develop": true,
"issue-*": false
}
}
}しかし、issue-27 ブランチにこの設定を追加しても、自動デプロイが実行され続けた。
原因
vercel.json は Vercel プロジェクトのデフォルトブランチ(通常は main)から読み込まれます。
feature ブランチに設定を追加しても、main または develop にマージされるまで Vercel には認識されません。
解決策
vercel.jsonをmainブランチにマージする- または、Vercel ダッシュボードから「Ignored Build Step」を設定する
問題 4: Turso データベースブランチの管理
要件
- PR ごとに独立したデータベースを作成
- staging データベースからブランチを作成
- PR クローズ時に自動削除
Turso のデータベースブランチ機能
Turso は --from-db オプションでデータベースの即時コピー(ブランチ)を作成できます:
turso db create my-new-database-branch --from-db my-existing-databaseこの操作は即座に完了し、元のデータベースの完全なコピーが作成されます。
GitHub Actions での実装
- name: Generate DB name from branch
id: db_name
run: |
BRANCH="${{ github.head_ref }}"
# ブランチ名を安全なDB名に変換(小文字、英数字とハイフンのみ、32文字以内)
DB_NAME="linto-$(echo "$BRANCH" | tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]-' | cut -c 1-32)"
echo "name=$DB_NAME" >> $GITHUB_OUTPUT
- name: Check if database exists
id: db_check
env:
TURSO_API_TOKEN: ${{ secrets.TURSO_API_TOKEN }}
run: |
if ~/.turso/turso db show ${{ steps.db_name.outputs.name }} > /dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Create Preview Database (branched from staging)
if: steps.db_check.outputs.exists == 'false'
env:
TURSO_API_TOKEN: ${{ secrets.TURSO_API_TOKEN }}
run: |
~/.turso/turso db create ${{ steps.db_name.outputs.name }} \
--from-db linto-staging \
--group previewPR クローズ時の削除
name: Preview DB Delete
on:
pull_request:
types: [closed]
branches: [develop]
jobs:
delete-preview-db:
runs-on: ubuntu-latest
steps:
- name: Install Turso CLI
run: curl -sSfL https://get.tur.so/install.sh | bash
- name: Generate DB name from branch
id: db_name
run: |
BRANCH="${{ github.head_ref }}"
DB_NAME="linto-$(echo "$BRANCH" | tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]-' | cut -c 1-32)"
echo "name=$DB_NAME" >> $GITHUB_OUTPUT
- name: Delete Preview Database
env:
TURSO_API_TOKEN: ${{ secrets.TURSO_API_TOKEN }}
run: |
~/.turso/turso db destroy ${{ steps.db_name.outputs.name }} --yes || true問題 5: マイグレーションワークフローのトリガー条件
元の設定
on:
push:
branches: [develop]
paths:
- "src/db/schema/**"
- "drizzle/**"問題点
src/db/schema/** を含めると、スキーマを変更しただけで(マイグレーションファイルを生成せずに)ワークフローが実行されてしまいます。
Drizzle のマイグレーションワークフロー
src/db/schema/** 変更
↓
drizzle-kit generate(ローカルで実行)
↓
drizzle/** にマイグレーションファイル生成
↓
git commit & push
↓
drizzle-kit migrate(CI で実行)drizzle-kit migrate はマイグレーションファイルを読み取って DB に適用するため、drizzle/** の変更のみをトリガーにすれば十分です。
修正後
on:
push:
branches: [develop]
paths:
- "drizzle/**"最終的なワークフロー構成
ワークフローファイル一覧
| ファイル | トリガー | 用途 |
|---|---|---|
preview-deploy.yml | PR → develop | プレビュー環境の作成・デプロイ |
preview-db-delete.yml | PR closed | プレビュー DB の削除 |
staging-migration.yml | push to develop(drizzle/**) | Staging DB へのマイグレーション |
production-migration.yml | 手動(workflow_dispatch) | Production DB へのマイグレーション |
環境変数とシークレット
| 名前 | 用途 | スコープ |
|---|---|---|
TURSO_API_TOKEN | Turso API 操作(DB 作成/削除) | Organization |
TURSO_GROUP_TOKEN | Preview グループへのアクセス | Preview |
TURSO_STAGING_DB_URL | Staging DB URL | Staging |
TURSO_PROD_DB_URL | Production DB URL | Production |
TURSO_PROD_DB_TOKEN | Production DB アクセス | Production |
VERCEL_TOKEN | Vercel API 操作 | All |
VERCEL_ORG_ID | Vercel Organization ID | All |
VERCEL_PROJECT_ID | Vercel Project ID | All |
BETTER_AUTH_SECRET | Better Auth セッション暗号化 | All |
GOOGLE_CLIENT_ID | Google OAuth Client ID | All |
GOOGLE_CLIENT_SECRET | Google OAuth Client Secret | All |
学んだこと
1. Vercel CLI と Git 連携の違いを理解する
| 機能 | Git 連携(自動デプロイ) | Vercel CLI |
|---|---|---|
| ブランチ検出 | 自動 | 手動(--meta) |
| ブランチ固有環境変数 | 自動適用 | 適用されない |
| 環境変数設定 | ダッシュボード/API | --env オプション |
2. OAuth のプレビュー環境対応
- 動的な redirect_uri は OAuth プロバイダーに登録できない
- プロキシパターンを使用して Production 経由でコールバックを処理
- クロスドメインの cookie 問題に注意
- カスタムドメイン使用時は
trustedOriginsと Google Cloud Console の更新が必要
3. データベースブランチの活用
- Turso のブランチ機能で即座にデータベースコピーを作成
- PR ライフサイクルに合わせた自動作成・削除
- グループを使用したアクセス制御
4. CI/CD でのマイグレーション管理
- スキーマ変更とマイグレーションファイル生成は分離
- マイグレーションファイルの変更のみをトリガーに
- Production マイグレーションは手動トリガーで安全に
まとめ
Vercel + Turso + Better Auth の組み合わせでプレビューデプロイメント環境を構築する際には、以下のポイントに注意が必要です:
- Vercel CLI デプロイでは
--envオプションで環境変数を直接渡す - Better Auth の oAuthProxy プラグインで OAuth のプレビュー対応
- Turso のデータベースブランチで PR ごとの独立した DB 環境
- 適切なトリガー条件でワークフローの無駄な実行を防止
- カスタムドメイン使用時は
PRODUCTION_URL、trustedOrigins、Google Cloud Console の設定を更新
これらの設定により、安全かつ効率的なプレビューデプロイメント環境を実現できます。