CI/CD

Turso + Drizzle マイグレーション運用ガイド:Vercel Hobby プラン対応

Vercel Hobby プランの制約下で、Turso データベースブランチングと Drizzle ORM を活用したマイグレーション戦略を詳しく解説します。develop ブランチと issue ブランチを区別する方法を紹介。

はじめに

データベースのマイグレーションは、アプリケーションへの影響が大きいため、実行タイミングを開発者がコントロールできることが重要です。本記事では、Vercel Hobby プランの制約を考慮しながら、Turso のデータベースブランチング機能と Drizzle ORM を組み合わせた堅牢なマイグレーション運用戦略を解説します。

Vercel Hobby プランの制約

Vercel Hobby プランでは、以下の環境のみ利用可能です:

環境ブランチドメイン
Productionmainlinto-dev.vercel.app
Preview全ての非 main ブランチプレビュー URL
DevelopmentCLI 経由なし

問題点: develop ブランチも issue-* ブランチも、すべて同じ「Preview」環境として扱われます。これでは、develop 用の永続データベースと issue 用の一時データベースを環境変数だけで区別できません。

解決策: 自動デプロイ無効化 + GitHub Actions 経由のデプロイ

本ガイドでは、以下の仕組みでこの問題を解決します:

  1. Turso グループトークン: 1 つのトークンで同グループ内の全データベースにアクセス
  2. 自動デプロイ無効化: vercel.jsongit.deploymentEnabled で feature ブランチの自動デプロイを無効化
  3. Vercel CLI: GitHub Actions から Vercel CLI を使用して、環境変数設定後にビルド・デプロイを実行
  4. アプリケーション変更不要: 既存の process.env.TURSO_DATABASE_URL をそのまま使用

アーキテクチャ概要

3 層データベース構成

環境データベース名ライフサイクルマイグレーション
Productionlinto-prod永続的手動トリガー
Staging (develop)linto-staging永続的develop マージ時に自動
Issue Previewlinto-issue-{番号}ブランチ単位で作成/削除PR 作成時に自動

トークン戦略

Turso ではグループトークンデータベース固有トークンを使い分けることで、セキュリティと利便性を両立できます。

根拠(Turso 公式ドキュメントより):

"With these tools, you have the flexibility to create tokens for a single database or all databases within a group."

トークン種別アクセス範囲用途
DB 固有トークン単一 DB のみProduction(最小権限)
グループトークングループ内全 DBPreview(運用効率)

これにより、環境変数で変更が必要なのは TURSO_DATABASE_URL のみとなり、Preview 環境ではトークンを共通で使用できます。


マイグレーション戦略

本ガイドでは、すべての環境で drizzle-kit generate + drizzle-kit migrate によるバージョン管理されたマイグレーションを採用します。

npx drizzle-kit generate  # マイグレーションファイル生成
npx drizzle-kit migrate   # マイグレーション適用

採用理由:

  • SQL マイグレーションファイルをバージョン管理できる
  • 変更履歴の追跡が可能
  • ロールバック計画が立てやすい
  • チームでのコードレビューが可能
  • ローカル・PR プレビュー・Staging・Production で一貫した手法を使用

事前準備

1. Turso グループとデータベースの作成

セキュリティ上の理由から、本番環境とプレビュー環境は別のグループに分離します。

Turso 公式ドキュメントより:

"With these tools, you have the flexibility to create tokens for a single database or all databases within a group."

グループトークンは同グループ内の全データベースにアクセスできます。そのため、本番とプレビューを同じグループに配置すると、プレビュー環境のトークンが漏洩した場合に本番データベースも危険にさらされます。

# Production グループの作成(本番専用)
turso group create production --location nrt

# Preview グループの作成(Staging + Feature ブランチ用)
turso group create preview --location nrt

# データベースの作成
turso db create linto-prod --group production
turso db create linto-staging --group preview

2. トークンの取得

# Production 用トークン(本番 DB 専用)
turso db tokens create linto-prod --expiration none

# Preview グループトークン(Staging + Feature DB 用)
turso group tokens create preview

# Platform API 用トークン(GitHub Actions での DB 作成/削除用)
turso auth api-tokens mint github-actions

トークン分離の利点:

トークンアクセス範囲用途
Production DB Tokenlinto-prod のみVercel Production 環境
Preview Group Tokenlinto-staging + 全 feature DBVercel Preview 環境
Platform API TokenDB の作成/削除操作GitHub Actions

3. vercel.json の設定(自動デプロイの制御)

issue ブランチの自動デプロイを無効化し、GitHub Actions からのデプロイのみを許可します。

Vercel 公式ドキュメントより:

"The git.deploymentEnabled configuration option allows you to specify which branches should not trigger a deployment upon commits."

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "git": {
    "deploymentEnabled": {
      "main": true,
      "develop": true,
      "issue-*": false
    }
  }
}
ブランチパターン自動デプロイ理由
main✅ 有効環境変数は Vercel ダッシュボードで事前設定済み
develop✅ 有効環境変数は Vercel ダッシュボードで事前設定済み
issue-*❌ 無効GitHub Actions 経由でデプロイ(DB 作成後)

4. GitHub Secrets の設定

シークレット名説明
TURSO_API_TOKENTurso Platform API トークン
TURSO_ORG_NAMETurso 組織名
TURSO_PROD_DB_TOKENProduction DB 専用トークン
TURSO_GROUP_TOKENPreview グループトークン
TURSO_STAGING_DB_URLStaging DB の URL
TURSO_PROD_DB_URLProduction DB の URL
VERCEL_TOKENVercel API トークン
VERCEL_ORG_IDVercel 組織 ID(vercel link 後に .vercel/project.json で確認可能)
VERCEL_PROJECT_IDVercel プロジェクト ID

5. Vercel 環境変数の設定

Vercel ダッシュボードで以下のように設定します。

Production 環境(main ブランチ)

変数名環境
TURSO_DATABASE_URLlibsql://linto-prod-{org}.turso.ioProduction
TURSO_AUTH_TOKEN{Production DB Token}Production

重要: Production 環境ではデータベース固有のトークンを使用します。これにより、万が一 Preview 環境のトークンが漏洩しても、本番データベースは保護されます。

Preview 環境(develop ブランチ専用)

Vercel では Preview 環境にブランチ固有の環境変数を設定できます:

  1. Vercel ダッシュボード → Settings → Environment Variables
  2. 新しい変数を追加
  3. Environment: Preview を選択
  4. "Add to specific Git Branch" をクリック
  5. ブランチ名に develop を入力
変数名環境ブランチ
TURSO_DATABASE_URLlibsql://linto-staging-{org}.turso.ioPreviewdevelop
TURSO_AUTH_TOKEN{Preview Group Token}Previewdevelop

Preview 環境(issue ブランチ用)

issue ブランチの TURSO_DATABASE_URLGitHub Actions が Vercel API 経由で自動設定します(後述のワークフロー参照)。

共通で必要な設定:

変数名環境
TURSO_AUTH_TOKEN{Preview Group Token}Preview

ポイント:

  • develop ブランチ固有の変数が、デフォルトの Preview 変数を上書きします
  • issue ブランチの TURSO_DATABASE_URL は PR 作成時に GitHub Actions が自動設定
  • PR クローズ時に GitHub Actions が自動削除
  • Preview Group Token は preview グループ内の全 DB(staging + issue)にアクセス可能ですが、production グループの DB にはアクセスできません

アプリケーション側の変更

変更不要

Vercel API のブランチ固有環境変数機能を活用することで、アプリケーション側のコード変更は一切不要です。

Vercel 公式ドキュメントより:

{
  "key": "TURSO_DATABASE_URL",
  "value": "libsql://linto-issue-1-org.turso.io",
  "target": ["preview"],
  "gitBranch": "issue-1"
}

gitBranch パラメータを指定することで、特定のブランチ専用の環境変数を設定できます。

この仕組みにより:

  1. Production (main): Vercel ダッシュボードで設定した TURSO_DATABASE_URL を使用
  2. Staging (develop): ブランチ固有の環境変数で linto-staging に接続
  3. issue ブランチ: GitHub Actions が Vercel API 経由でブランチ固有の環境変数を自動設定

既存の src/db/index.ts は変更せず、単純に process.env.TURSO_DATABASE_URL を参照するだけで OK です。


Google OAuth のプレビュー環境対応

問題点

Google OAuth では、Authorized redirect URIs に事前登録した URL にしかコールバックできません。しかし、Vercel のプレビュー環境では以下のような動的な URL が生成されます:

  • https://linto-dev-abc123-username.vercel.app
  • https://linto-dev-feature-login-xyz.vercel.app

これらすべてを Google Cloud Console に事前登録することは不可能です。

解決策: Better Auth の oAuthProxy プラグイン

Better Auth には oAuthProxy プラグインが用意されており、この問題を解決できます。

Better Auth 公式ドキュメントより:

"A proxy plugin, that allows you to proxy OAuth requests. Useful for development and preview deployments where the redirect URL can't be known in advance to add to the OAuth provider."

動作原理

実装方法

1. src/lib/auth.ts を更新:

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { oAuthProxy } from "better-auth/plugins";
import { db } from "@/db";
import { env } from "@/env";

export const auth = betterAuth({
  baseURL: env.BETTER_AUTH_URL,
  database: drizzleAdapter(db, {
    provider: "sqlite",
  }),
  plugins: [
    oAuthProxy({
      // Production 環境の URL(Google に登録した redirect URI のベース)
      productionURL: "https://linto-dev.vercel.app",
    }),
  ],
  socialProviders: {
    google: {
      clientId: env.GOOGLE_CLIENT_ID,
      clientSecret: env.GOOGLE_CLIENT_SECRET,
      // redirect URI を明示的に Production に設定
      redirectURI: "https://linto-dev.vercel.app/api/auth/callback/google",
    },
  },
});

export type Auth = typeof auth;
export type Session = typeof auth.$Infer.Session;

2. Google Cloud Console の設定:

Google Cloud Console の Authorized redirect URIs には、Production URL のみを登録します:

https://linto-dev.vercel.app/api/auth/callback/google

Preview 環境や localhost の URL は登録不要です。

3. 環境変数の設定:

各環境で BETTER_AUTH_URL を適切に設定します:

環境BETTER_AUTH_URL
Production (main)https://linto-dev.vercel.app
Preview (develop)https://linto-dev-git-develop-{username}.vercel.app
Preview (issue)https://linto-dev-{branch}-{username}.vercel.app
Localhttp://localhost:3000

ポイント: BETTER_AUTH_URL は現在のデプロイ URL を設定します。oAuthProxy プラグインがこれを検出し、Production 以外の場合はプロキシ経由で認証を行います。

Vercel での環境変数設定

Vercel では VERCEL_URL システム環境変数を使用して動的に BETTER_AUTH_URL を設定できます:

// src/env.ts
export const env = createEnv({
  server: {
    // Vercel では VERCEL_URL から自動構築、ローカルでは明示的に設定
    BETTER_AUTH_URL: z.string().url().default(
      process.env.VERCEL_URL
        ? `https://${process.env.VERCEL_URL}`
        : "http://localhost:3000"
    ),
    // ...
  },
});

または、Vercel の Environment Variables で:

変数名環境
BETTER_AUTH_URLhttps://linto-dev.vercel.appProduction
BETTER_AUTH_URLhttps://$VERCEL_URLPreview

セキュリティ上の注意

oAuthProxy は以下のセキュリティ機能を提供します:

  1. Cookie の暗号化: Production サーバーでセッション Cookie を暗号化してから Preview 環境に転送
  2. Secret の共有: BETTER_AUTH_SECRET が全環境で同じである必要がある
  3. オリジン検証: 信頼できるオリジンからのリクエストのみを受け入れる

重要: BETTER_AUTH_SECRET は全環境(Production, Preview, Local)で同じ値を使用してください。異なる値だと Cookie の復号化に失敗します。


GitHub Actions ワークフロー

1. issue ブランチ用データベース作成 + Vercel デプロイ

Vercel 公式ドキュメントより:

"Use vercel build inside GitHub Actions to build your project without exposing source code to Vercel, and finally vercel deploy --prebuilt to skip the build step on Vercel."

.github/workflows/preview-deploy.yml:

name: Preview Deploy

on:
  pull_request:
    types: [opened, reopened, synchronize]
    branches: [develop]

env:
  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

jobs:
  deploy-preview:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Install Turso CLI
        run: curl -sSfL https://get.tur.so/install.sh | bash

      - name: Install Vercel CLI
        run: npm install -g vercel@latest

      # ========================================
      # Step 1: データベースの作成(初回のみ)
      # ========================================
      - 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: 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

      - name: Get Preview Database URL
        id: db_url
        env:
          TURSO_API_TOKEN: ${{ secrets.TURSO_API_TOKEN }}
        run: |
          DB_URL=$(~/.turso/turso db show ${{ steps.db_name.outputs.name }} --url)
          echo "url=$DB_URL" >> $GITHUB_OUTPUT

      # ========================================
      # Step 2: マイグレーションの適用
      # ========================================
      - name: Apply migrations
        env:
          TURSO_DATABASE_URL: ${{ steps.db_url.outputs.url }}
          TURSO_AUTH_TOKEN: ${{ secrets.TURSO_GROUP_TOKEN }}
        run: npx drizzle-kit migrate

      # ========================================
      # Step 3: Vercel 環境変数の設定
      # ========================================
      - name: Set Vercel environment variable for branch
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
        run: |
          curl -X POST "https://api.vercel.com/v10/projects/$VERCEL_PROJECT_ID/env?upsert=true" \
            -H "Authorization: Bearer $VERCEL_TOKEN" \
            -H "Content-Type: application/json" \
            -d '{
              "key": "TURSO_DATABASE_URL",
              "value": "${{ steps.db_url.outputs.url }}",
              "type": "encrypted",
              "target": ["preview"],
              "gitBranch": "${{ github.head_ref }}"
            }'

      # ========================================
      # Step 4: Vercel CLI でビルド & デプロイ
      # ========================================
      - name: Pull Vercel Environment
        run: vercel pull --yes --environment=preview --git-branch=${{ github.head_ref }} --token=${{ secrets.VERCEL_TOKEN }}

      - name: Build Project
        run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
        env:
          TURSO_DATABASE_URL: ${{ steps.db_url.outputs.url }}
          TURSO_AUTH_TOKEN: ${{ secrets.TURSO_GROUP_TOKEN }}

      - name: Deploy to Vercel
        id: deploy
        run: |
          DEPLOY_URL=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
          echo "url=$DEPLOY_URL" >> $GITHUB_OUTPUT

      # ========================================
      # Step 5: PR にコメント
      # ========================================
      - name: Comment PR with deployment info
        if: github.event.action == 'opened'
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## 🚀 Preview Deployed

              | Item | Value |
              |------|-------|
              | Preview URL | ${{ steps.deploy.outputs.url }} |
              | Database | \`${{ steps.db_name.outputs.name }}\` |
              | Branch | \`${{ github.head_ref }}\` |

              ✅ Database created from \`linto-staging\`
              ✅ Migrations applied
              ✅ Deployed with correct environment variables

              **Note**: Database will be deleted when PR is closed.
              `
            })

ポイント:

  1. synchronize イベント: PR への追加 push でも自動デプロイ
  2. DB 存在チェック: 2 回目以降は DB 作成をスキップ
  3. Vercel CLI: vercel pullvercel buildvercel deploy --prebuilt の順で実行
  4. 環境変数設定後にビルド: 競合状態なし、再デプロイ不要

2. PR クローズ時のデータベース削除

.github/workflows/preview-db-delete.yml:

name: Delete Preview Database

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

      - name: Get Vercel environment variable ID
        id: env_id
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
          VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
          BRANCH_NAME: ${{ github.head_ref }}
        run: |
          TEAM_PARAM=""
          if [ -n "$VERCEL_TEAM_ID" ]; then
            TEAM_PARAM="?teamId=$VERCEL_TEAM_ID"
          fi

          # ブランチ固有の環境変数を検索
          # 注意: jq 内で環境変数を使用するため --arg を使用
          ENV_ID=$(curl -s "https://api.vercel.com/v9/projects/$VERCEL_PROJECT_ID/env$TEAM_PARAM" \
            -H "Authorization: Bearer $VERCEL_TOKEN" \
            | jq -r --arg branch "$BRANCH_NAME" '.envs[] | select(.key == "TURSO_DATABASE_URL" and .gitBranch == $branch) | .id')

          echo "id=$ENV_ID" >> $GITHUB_OUTPUT

      - name: Delete Vercel environment variable
        if: steps.env_id.outputs.id != ''
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
          VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
        run: |
          TEAM_PARAM=""
          if [ -n "$VERCEL_TEAM_ID" ]; then
            TEAM_PARAM="?teamId=$VERCEL_TEAM_ID"
          fi

          curl -X DELETE "https://api.vercel.com/v9/projects/$VERCEL_PROJECT_ID/env/${{ steps.env_id.outputs.id }}$TEAM_PARAM" \
            -H "Authorization: Bearer $VERCEL_TOKEN"

          echo "Vercel environment variable deleted for branch: ${{ github.head_ref }}"

      - name: Comment PR
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Preview Database Deleted

              The preview database \`${{ steps.db_name.outputs.name }}\` and its Vercel environment variable have been deleted.
              `
            })

3. Staging(develop)へのマイグレーション

.github/workflows/staging-migration.yml:

name: Staging Migration

on:
  push:
    branches: [develop]
    paths:
      - "src/db/schema/**"
      - "drizzle/**"

jobs:
  migrate-staging:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run migrations on Staging
        env:
          TURSO_DATABASE_URL: ${{ secrets.TURSO_STAGING_DB_URL }}
          TURSO_AUTH_TOKEN: ${{ secrets.TURSO_GROUP_TOKEN }}
        run: npx drizzle-kit migrate

      - name: Notify success
        if: success()
        run: echo "Staging migration completed successfully"

4. Production マイグレーション(手動トリガー)

.github/workflows/production-migration.yml:

name: Production Migration

on:
  workflow_dispatch:
    inputs:
      confirm:
        description: 'Type "migrate-production" to confirm'
        required: true
        type: string
      dry_run:
        description: "Dry run (show pending migrations without applying)"
        required: false
        type: boolean
        default: true

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Validate confirmation
        if: github.event.inputs.confirm != 'migrate-production'
        run: |
          echo "::error::Confirmation text does not match. Please type 'migrate-production' to proceed."
          exit 1

  migrate-production:
    needs: validate
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: main

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Check pending migrations
        env:
          TURSO_DATABASE_URL: ${{ secrets.TURSO_PROD_DB_URL }}
          TURSO_AUTH_TOKEN: ${{ secrets.TURSO_PROD_DB_TOKEN }}
        run: |
          echo "Checking for pending migrations..."
          npx drizzle-kit check

      - name: Show pending migrations (if dry run)
        if: github.event.inputs.dry_run == 'true'
        run: |
          echo "=== DRY RUN MODE ==="
          echo "Pending migration files:"
          ls -la drizzle/*.sql 2>/dev/null || echo "No migration files found"
          echo ""
          echo "Migration journal:"
          cat drizzle/meta/_journal.json 2>/dev/null || echo "No journal found"
          echo "=== END DRY RUN ==="

      - name: Run migrations on Production
        if: github.event.inputs.dry_run == 'false'
        env:
          TURSO_DATABASE_URL: ${{ secrets.TURSO_PROD_DB_URL }}
          TURSO_AUTH_TOKEN: ${{ secrets.TURSO_PROD_DB_TOKEN }}
        run: |
          echo "Running production migrations..."
          npx drizzle-kit migrate

運用フロー図


技術的根拠

処理順序とタイミングの重要性

Vercel 公式ドキュメントより:

"Changes to environment variables are not applied to previous deployments, they only apply to new deployments. You must redeploy your project to update the value of any variables you change in the deployment."

自動デプロイを有効にしたまま環境変数を後から設定すると、競合状態が発生します。本ガイドでは、以下の方法でこの問題を解決します:

解決策: 自動デプロイ無効化 + Vercel CLI

Vercel 公式ドキュメントより:

"The git.deploymentEnabled configuration option allows you to specify which branches should not trigger a deployment upon commits."

"Use vercel build inside GitHub Actions to build your project without exposing source code to Vercel, and finally vercel deploy --prebuilt to skip the build step on Vercel."

ポイント:

  1. 自動デプロイ無効: issue ブランチは vercel.json で自動デプロイを無効化
  2. 順序保証: GitHub Actions が DB 作成 → マイグレーション → 環境変数設定 → ビルド → デプロイの順序を保証
  3. 単一デプロイ: 再デプロイ不要、効率的

1. Vercel ブランチ固有環境変数

Vercel 公式ドキュメントより:

"Preview environment variables are applied to deployments from any Git branch that does not match the Production Branch. When you add a preview environment variable, you can choose to apply it to all non-production branches or select a specific branch. Any branch-specific variables will override other preview environment variables with the same name."

これにより、develop ブランチ専用の環境変数を設定でき、他の feature ブランチとは異なるデータベースに接続できます。

2. Turso グループ分離とトークン戦略

Turso 公式ドキュメントより:

"With these tools, you have the flexibility to create tokens for a single database or all databases within a group."

グループトークンは同グループ内の全データベースにアクセスできるため、セキュリティ上の理由から Production と Preview を別グループに分離します:

グループ含まれる DBトークンリスク範囲
productionlinto-prod のみDB 固有トークン本番 DB のみ
previewlinto-staging + 全 issue DBグループトークンPreview 環境のみ

分離の利点:

  • Preview 環境のトークンが漏洩しても、本番 DB にはアクセス不可
  • Preview グループ内では 1 つのトークンで全 DB にアクセス可能(管理が簡素化)
  • 本番 DB は独立したトークンで厳重に保護

3. Vercel API ブランチ固有環境変数

Vercel 公式ドキュメントより:

"gitBranch - The git branch name for this Environment Variable to be associated with. This property can be used to create environment variables that are specific to a git branch."

Vercel API の POST /v10/projects/{idOrName}/env エンドポイントでは、gitBranch パラメータを指定することで特定のブランチ専用の環境変数を設定できます:

{
  "key": "TURSO_DATABASE_URL",
  "value": "libsql://linto-issue-1-org.turso.io",
  "type": "encrypted",
  "target": ["preview"],
  "gitBranch": "issue-1"
}

これにより:

  • アプリケーション側の変更不要: 既存の process.env.TURSO_DATABASE_URL をそのまま使用
  • GitHub Actions で自動化: PR 作成時に環境変数を自動設定、PR クローズ時に自動削除
  • セキュリティ: type: "encrypted" でトークンを暗号化して保存

4. Better Auth oAuthProxy プラグイン

Better Auth 公式ドキュメントより:

"A proxy plugin, that allows you to proxy OAuth requests. Useful for development and preview deployments where the redirect URL can't be known in advance to add to the OAuth provider."

oAuthProxy プラグインにより:

  • Google OAuth の redirect URI は Production URL のみ登録すれば OK
  • Preview/Development 環境では Production 経由でプロキシ認証
  • Cookie は暗号化されて転送されるためセキュリティも確保
  • クライアント側のコード変更は不要

ベストプラクティス

1. マイグレーションファイルの管理

# マイグレーションファイルの生成
npx drizzle-kit generate

# 生成されたファイルを確認
ls drizzle/

# コミットに含める
git add drizzle/
git commit -m "Add migration: add user profile fields"

2. 破壊的変更への対処

カラム削除やテーブル削除などの破壊的変更は、2 段階で実行:

Phase 1: 準備

// 1. アプリケーションコードから該当カラムの参照を削除
// 2. デプロイ

Phase 2: マイグレーション

// 3. カラム削除のマイグレーションを実行

3. ロールバック手順の文書化

各マイグレーションに対して、ロールバック SQL を用意:

-- drizzle/0001_add_profile.sql
ALTER TABLE users ADD COLUMN profile_image TEXT;

-- drizzle/0001_add_profile_rollback.sql (手動で作成)
ALTER TABLE users DROP COLUMN profile_image;

4. マイグレーション前のバックアップ

# production-migration.yml に追加
- name: Create backup before migration
  env:
    TURSO_API_TOKEN: ${{ secrets.TURSO_API_TOKEN }}
  run: |
    BACKUP_NAME="linto-prod-backup-$(date +%Y%m%d-%H%M%S)"
    # バックアップは preview グループに作成(本番トークンのスコープを限定)
    ~/.turso/turso db create $BACKUP_NAME --from-db linto-prod --group preview
    echo "Backup created: $BACKUP_NAME"

注意: バックアップを preview グループに作成することで、Preview 環境のトークンでバックアップにアクセスできます。本番グループに作成すると、本番トークンのスコープが広がるリスクがあります。


トラブルシューティング

issue ブランチのデータベースが見つからない

  1. GitHub Actions のログで DB 作成が成功しているか確認
  2. ブランチ名の sanitize ロジックを確認(特殊文字、長さ制限)
  3. Turso ダッシュボードでデータベースの存在を確認

develop ブランチが issue 用 DB に接続してしまう

  1. Vercel のブランチ固有環境変数が正しく設定されているか確認
  2. develop ブランチに TURSO_DATABASE_URL が設定されているか確認
  3. 環境変数の優先順位を確認(ブランチ固有 > Preview デフォルト)

マイグレーションが失敗する

  1. npx drizzle-kit check でスキーマの整合性を確認
  2. ローカルで npx drizzle-kit generate を実行し、生成されるマイグレーションファイルを確認
  3. 依存関係のあるテーブルの順序を確認
  4. マイグレーションファイルの SQL 構文を確認

まとめ

Vercel Hobby プランの制約下でも、以下の方法で develop ブランチと issue ブランチを区別できます:

環境ブランチデータベースデプロイ方法
Productionmainlinto-prodVercel 自動デプロイ
Stagingdeveloplinto-stagingVercel 自動デプロイ
Issue Previewissue-*linto-issue-{番号}GitHub Actions + Vercel CLI

キーポイント:

  1. 自動デプロイ制御: vercel.jsongit.deploymentEnabled で issue ブランチの自動デプロイを無効化
  2. Vercel CLI: GitHub Actions から vercel build + vercel deploy --prebuilt で順序を完全制御
  3. 競合状態の回避: 環境変数設定後にビルド・デプロイを実行するため、再デプロイ不要
  4. グループ分離: Production と Preview を別グループに分離してセキュリティを確保
  5. アプリケーション変更不要: 既存の process.env.TURSO_DATABASE_URL をそのまま使用

この構成により、再デプロイなし・アプリケーション変更なしで、Hobby プランでも本格的なマイグレーション運用が可能になります。

目次

はじめにVercel Hobby プランの制約解決策: 自動デプロイ無効化 + GitHub Actions 経由のデプロイアーキテクチャ概要3 層データベース構成トークン戦略マイグレーション戦略事前準備1. Turso グループとデータベースの作成2. トークンの取得3. vercel.json の設定(自動デプロイの制御)4. GitHub Secrets の設定5. Vercel 環境変数の設定Production 環境(main ブランチ)Preview 環境(develop ブランチ専用)Preview 環境(issue ブランチ用)アプリケーション側の変更変更不要Google OAuth のプレビュー環境対応問題点解決策: Better Auth の oAuthProxy プラグイン動作原理実装方法Vercel での環境変数設定セキュリティ上の注意GitHub Actions ワークフロー1. issue ブランチ用データベース作成 + Vercel デプロイ2. PR クローズ時のデータベース削除3. Staging(develop)へのマイグレーション4. Production マイグレーション(手動トリガー)運用フロー図技術的根拠処理順序とタイミングの重要性解決策: 自動デプロイ無効化 + Vercel CLI1. Vercel ブランチ固有環境変数2. Turso グループ分離とトークン戦略3. Vercel API ブランチ固有環境変数4. Better Auth oAuthProxy プラグインベストプラクティス1. マイグレーションファイルの管理2. 破壊的変更への対処3. ロールバック手順の文書化4. マイグレーション前のバックアップトラブルシューティングissue ブランチのデータベースが見つからないdevelop ブランチが issue 用 DB に接続してしまうマイグレーションが失敗するまとめ