CI/CD

Vercel + Turso + Better Auth:プレビューデプロイメント完全ガイド

Vercel プレビューデプロイメントで Turso データベースブランチと Better Auth OAuth を正しく動作させるための包括的なガイド。発生した問題と解決策を詳しく解説します。

はじめに

モダンな Web アプリケーション開発では、プルリクエストごとにプレビュー環境を自動構築することが一般的になっています。しかし、データベースOAuth 認証を含むプレビュー環境の構築には、いくつかの技術的な課題があります。

本記事では、以下の技術スタックでプレビューデプロイメント環境を構築する際に遭遇した問題と、その解決策を詳しく解説します:

  • Vercel: ホスティング・デプロイメント
  • Turso: SQLite 互換の分散データベース
  • Better Auth: TypeScript 向け認証フレームワーク
  • GitHub Actions: CI/CD パイプライン

目標とするアーキテクチャ

プレビュー環境の要件

  1. ブランチごとに独立したデータベース: 各プレビュー環境が独自のデータベースを持つ
  2. OAuth 認証の動作: プレビュー URL でも Google 認証が正常に動作する
  3. 自動化されたワークフロー: 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 ago

2. ブランチ固有の環境変数の取得テスト

$ 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 連携を使用しないため:

  1. Git 情報の欠如: CLI デプロイには Git のブランチ情報が自動的に含まれない
  2. --meta gitBranchの限界: メタデータとして記録されるだけで、環境変数の選択には影響しない
  3. 設計上の分離: 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,
  },
  // ...
});

なぜ必要なのか

  1. プレビューサーバーで OAuth 開始: state cookie はプレビュードメインに設定される
  2. Google が Production にリダイレクト: callback は Production URL に送られる
  3. クロスドメインの cookie 問題: Production サーバーはプレビュードメインの cookie にアクセスできない
  4. state 検証が失敗: 通常の検証フローでは認証が失敗する

セキュリティ考慮事項

skipStateCookieCheck を有効にすることで CSRF 保護が弱まる可能性がありますが、以下の対策で軽減しています:

  1. trustedOrigins の制限: https://*.vercel.app のみを信頼
  2. Production URL の固定: redirect_uri は常に Production URL
  3. 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用途
Productionhttps://example.comカスタムドメイン(Google に登録)
Production (Vercel)https://your-project.vercel.appVercel デフォルト(リダイレクト設定推奨)
Previewhttps://your-project-git-branch.vercel.appプレビュー環境
Localhttp://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 には認識されません。

解決策

  1. vercel.jsonmain ブランチにマージする
  2. または、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 preview

PR クローズ時の削除

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.ymlPR → developプレビュー環境の作成・デプロイ
preview-db-delete.ymlPR closedプレビュー DB の削除
staging-migration.ymlpush to develop(drizzle/**)Staging DB へのマイグレーション
production-migration.yml手動(workflow_dispatch)Production DB へのマイグレーション

環境変数とシークレット

名前用途スコープ
TURSO_API_TOKENTurso API 操作(DB 作成/削除)Organization
TURSO_GROUP_TOKENPreview グループへのアクセスPreview
TURSO_STAGING_DB_URLStaging DB URLStaging
TURSO_PROD_DB_URLProduction DB URLProduction
TURSO_PROD_DB_TOKENProduction DB アクセスProduction
VERCEL_TOKENVercel API 操作All
VERCEL_ORG_IDVercel Organization IDAll
VERCEL_PROJECT_IDVercel Project IDAll
BETTER_AUTH_SECRETBetter Auth セッション暗号化All
GOOGLE_CLIENT_IDGoogle OAuth Client IDAll
GOOGLE_CLIENT_SECRETGoogle OAuth Client SecretAll

学んだこと

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 の組み合わせでプレビューデプロイメント環境を構築する際には、以下のポイントに注意が必要です:

  1. Vercel CLI デプロイでは --env オプションで環境変数を直接渡す
  2. Better Auth の oAuthProxy プラグインで OAuth のプレビュー対応
  3. Turso のデータベースブランチで PR ごとの独立した DB 環境
  4. 適切なトリガー条件でワークフローの無駄な実行を防止
  5. カスタムドメイン使用時は PRODUCTION_URLtrustedOrigins、Google Cloud Console の設定を更新

これらの設定により、安全かつ効率的なプレビューデプロイメント環境を実現できます。


参考リンク

目次

はじめに目標とするアーキテクチャプレビュー環境の要件全体フロー問題 1: Vercel CLI デプロイで環境変数が適用されない発生した症状調査プロセス1. Vercel ダッシュボードの確認2. ブランチ固有の環境変数の取得テスト3. デプロイメントの検証根本原因なぜこの仕様なのか解決策各アプローチの比較問題 2: OAuth 認証がプレビュー環境で動作しない発生した症状原因Better Auth の oAuthProxy プラグイン動作原理設定方法重要な設定: skipStateCookieCheckなぜ必要なのかセキュリティ考慮事項カスタムドメイン使用時の設定1. PRODUCTION_URL の変更2. trustedOrigins の更新3. Google Cloud Console の設定更新4. 環境別 URL の対応表5. Vercel でのリダイレクト設定カスタムドメイン移行時のチェックリスト問題 3: vercel.json の設定が反映されない発生した症状原因解決策問題 4: Turso データベースブランチの管理要件Turso のデータベースブランチ機能GitHub Actions での実装PR クローズ時の削除問題 5: マイグレーションワークフローのトリガー条件元の設定問題点Drizzle のマイグレーションワークフロー修正後最終的なワークフロー構成ワークフローファイル一覧環境変数とシークレット学んだこと1. Vercel CLI と Git 連携の違いを理解する2. OAuth のプレビュー環境対応3. データベースブランチの活用4. CI/CD でのマイグレーション管理まとめ参考リンク