---
title: "データストレージ移行計画"
---

## 背景と問題

### 現状の課題

現在のアーキテクチャでは `data/members.json` をプロジェクトリポジトリ内で管理し、Next.js API Route (`app/api/members/route.ts`) から `fs.writeFileSync` で書き込んでいる。

Vercel のサーバーレス関数は `/var/task`（プロジェクトルート）が **読み取り専用** となっており、`fs.writeFileSync` は `EROFS: read-only file system` エラーで失敗する。

その結果、**ローカル開発環境では動作するが、本番（Vercel デプロイ済み環境）では編集内容を保存できない。**

現在の暫定対応として、書き込みエラー時に HTTP 503 を返してユーザーに通知する処理を実装しているが、根本的な解決には外部ストレージへの移行が必要である。

### 関連情報

- 開発ノート 実装 #7「Vercel 本番環境でのファイル書き込み制限」
- GitHub Issue #69「編集保存機能改善」

---

## 現在のアーキテクチャ

```
┌─────────────────────────────────────────────┐
│ GitHub リポジトリ                            │
│  data/members.json  ←── git commit          │
│         │                    ▲              │
│         │              GitHub Actions       │
│         │              sync-members.yml     │
│         │              (毎日 JST 09:00)      │
└─────────┼────────────────────┼──────────────┘
          │ (静的ファイル)      │ (コミット)
          ▼                    │
┌─────────────────────┐   scripts/sync-status.ts
│ Vercel デプロイ      │
│  Next.js App Router │
│  /api/members (GET) │ ← JSON 読み取り ✅
│  /api/members (PUT) │ ← JSON 書き込み ❌ (EROFS)
└─────────────────────┘
```

---

## 移行先オプションの比較

### オプション A: Vercel Blob（推奨）

`@vercel/blob` パッケージを使い、`members.json` の内容を Blob ストレージに保存する。

**移行後のデータフロー:**

```
Vercel Blob
  members.json (blob)
        │
        ├── GET  /api/members  → blob.downloadUrl から JSON 取得
        ├── PUT  /api/members  → JSON を上書き保存
        └── sync-status.ts    → Blob を上書き更新
```

| 観点 | 評価 |
|------|------|
| 移行コスト | 低い（JSON 形式を維持、API 変更が最小） |
| 既存データ形式の互換性 | ◎ そのまま利用可能 |
| 同期スクリプトへの影響 | 小（`@vercel/blob` SDK でアップロードに変更） |
| インフラ追加 | Vercel Blob のみ（同一プラットフォーム） |
| コスト | 無料枠: 5 GB ストレージ・1 GB 転送/月 |
| バージョン管理 | なし（git での日次コミット履歴は失われる） |

> **バージョン管理喪失のリスクと代替手段**
>
> 現在の `data/members.json` は GitHub Actions による毎日の自動コミットで変更履歴が git に蓄積されており、特定日時へのロールバックや変更追跡が可能である。Blob 移行後はこれらが失われる。
>
> **失われるもの:**
> - git での日次コミット履歴（ロールバック手段・変更追跡）
>
> **代替手段:**
> - Vercel Blob は上書き保存のみで、標準的なバージョニング機能は持たない（`@vercel/blob` の `list()` で同名ファイルの過去バージョンは取得できない）
> - 重要な変更前には手動で JSON のスナップショットを別ファイル名として Blob に保存する運用を補完策として採用する
>
> **「重要な変更」の例:**
> - バグ修正による一括データ更新（statusHistory の整形など）
> - メンバーデータ構造の変更（新フィールド追加など）
> - 移行フェーズ実施前（Blob 移行の各フェーズ開始前）
>
> **スナップショット管理方針:**
> - ファイル名に ISO 日時を含める（例: `members-backup-YYYYMMDDTHHmm.json`）
> - 直近 3 件のスナップショットを保持する。新たなスナップショット作成後、4件目以降の古いものを `del()` で削除する
> - 変更追跡が必要になった場合は、将来的にオプション C（Vercel Postgres）への再移行を検討する

**必要な環境変数:**

| 変数名 | 用途 | 取得タイミング |
|--------|------|----------------|
| `BLOB_READ_WRITE_TOKEN` | Blob への読み書き権限 | Vercel ダッシュボードで Blob ストア作成時に自動生成 |
| `MEMBERS_BLOB_URL` | members.json の Blob URL | フェーズ 4 の初期データ移行スクリプト実行後に取得 |

> **注意:** `MEMBERS_BLOB_URL` はフェーズ 4 完了後でないと確定しない。フェーズ 2・3 の実装時はプレースホルダーとして設定し、フェーズ 4 実行後に正式な URL に差し替えること。

---

### オプション B: Vercel KV（Redis）

`@vercel/kv` を使い、メンバーを `member:{id}` というキーで個別に管理する。

| 観点 | 評価 |
|------|------|
| 移行コスト | 中（データ構造の変更が必要） |
| 既存データ形式の互換性 | △ 全件取得に `SCAN` が必要で複雑 |
| 同期スクリプトへの影響 | 中（各メンバーを個別に SET） |
| インフラ追加 | Vercel KV のみ |
| コスト | 無料枠: 256 MB |

「全メンバー一覧取得」のパターンが多いこのアプリには不向き。

---

### オプション C: Vercel Postgres（Neon）

PostgreSQL を使ってメンバーデータをリレーショナルに管理する。

| 観点 | 評価 |
|------|------|
| 移行コスト | 高い（スキーマ設計・Prisma 導入が必要） |
| 既存データ形式の互換性 | △ JSON → テーブル変換が必要 |
| 同期スクリプトへの影響 | 大（SQL INSERT/UPDATE に変更） |
| 拡張性 | ◎ 将来的な機能拡張に対応しやすい |
| コスト | 無料枠: 0.5 GB |

現在の規模（47 名）には過剰。将来的にディスコグラフィー等（Issue #56）を追加する場合は有力候補。

---

### オプション D: GitHub API 経由でのコミット

編集 API が GitHub Contents API を呼び出してリポジトリに直接コミットする方式。

| 観点 | 評価 |
|------|------|
| 移行コスト | 中（GitHub Personal Access Token が必要） |
| 既存データ形式の互換性 | ◎ |
| リアルタイム性 | × 保存後に再デプロイが必要 |
| レート制限 | あり（GitHub API） |

保存後すぐに画面に反映されないため UX が悪く、このアプリには不向き。

---

## 推奨案: オプション A（Vercel Blob）

### 選定理由

1. **移行コストが最小**: JSON ファイルをそのまま Blob に置き換えるだけで、データ構造・型定義の変更が不要。
2. **同期スクリプトとの親和性が高い**: `sync-status.ts` は現在 `fs.writeFileSync` でファイルを更新しているが、`put()` 関数に変更するだけで対応できる。
3. **同一プラットフォーム**: Vercel プロジェクトと統合されており、追加インフラ管理が不要。
4. **無料枠で十分**: 現在のデータ量（`members.json` は約 200 KB 程度）は無料枠の範囲内。

---

## 実装計画

### フェーズ 1: Vercel Blob の設定

1. Vercel ダッシュボードで Blob ストア（`morning-status-app-store` 等）を作成する。
2. `BLOB_READ_WRITE_TOKEN` 環境変数を Vercel プロジェクトと GitHub Actions Secrets に登録する。
3. パッケージを追加する:
   ```bash
   bun add @vercel/blob
   ```

### フェーズ 2: API Route の移行

`app/api/members/route.ts` の `fs.readFileSync` / `fs.writeFileSync` を `@vercel/blob` の API に置き換える。

**GET（読み取り）:**
```ts
export async function GET() {
  const res = await fetch(process.env.MEMBERS_BLOB_URL!);
  const data = await res.json();
  return NextResponse.json(data);
}
```

**PUT（書き込み）:**
```ts
import { put } from "@vercel/blob";

export async function PUT(request: Request) {
  const updatedMember: Member = await request.json();
  // 既存データを取得して更新し、Blob に PUT
  const res = await fetch(process.env.MEMBERS_BLOB_URL!);
  const data = await res.json();
  // ... 更新処理 ...
  await put("members.json", JSON.stringify(data, null, 2), {
    access: "public",
    contentType: "application/json",
    addRandomSuffix: false,
  });
  return NextResponse.json({ message: "Member updated successfully", member: updatedMember });
}
```

> **注意:** `put()` はデフォルトで新しい URL を発行する。固定 URL を使いたい場合は `addRandomSuffix: false` を指定するか、URL を環境変数で管理する。Vercel Blob の `copy()` と `del()` を組み合わせる方式も検討すること。

### フェーズ 3: 同期スクリプトの移行

`scripts/sync-status.ts` の `fs.writeFileSync` を Blob の `put()` に置き換える。

```ts
import { put } from "@vercel/blob";
// ...
await put("members.json", JSON.stringify(data, null, 2), {
  access: "public",
  contentType: "application/json",
  addRandomSuffix: false, // 固定ファイル名
  token: process.env.BLOB_READ_WRITE_TOKEN,
});
```

GitHub Actions の `sync-members.yml` では `git add / commit` の代わりに Blob への書き込みのみで完結するため、`contents: write` パーミッションが不要になる。

#### `data/members.json` の凍結方針

移行後、git 上の `data/members.json` は**移行時点の内容で凍結**する。

- 同期スクリプトは Blob のみを更新し、git へのコミットは行わない
- `data/members.json` はビルド時の SSG 初期データとしてのみ使用する（フェーズ 5 で `revalidatePath` / ISR を導入することで、本番データは Blob を正として扱う）
- 凍結後は `data/members.json` への手動コミットは行わないこと（Blob との乖離が拡大し、次回ビルド時に古いデータが SSG ページを上書きするリスクがある）

> **補足:** フェーズ 5（SSG 対応）を完了させることで、一覧・詳細ページともに Blob のデータが正となり、git の JSON との乖離が問題にならなくなる。フェーズ 5 未実施のまま運用する場合は、詳細ページのデータが徐々に古くなる点に注意すること。

### フェーズ 4: 初期データ移行

```ts
// 一度だけ実行するスクリプト（scripts/migrate-to-blob.ts）
import { put } from "@vercel/blob";
import fs from "fs";
import path from "path";

const data = fs.readFileSync(path.join(process.cwd(), "data", "members.json"), "utf-8");
const result = await put("members.json", data, {
  access: "public",
  contentType: "application/json",
  addRandomSuffix: false,
  token: process.env.BLOB_READ_WRITE_TOKEN,
});
console.log("Blob URL:", result.url);
// → この URL を MEMBERS_BLOB_URL 環境変数に設定する
```

### フェーズ 5: 静的ビルドの対応（推奨）

現在 `generateStaticParams` で SSG しているメンバー詳細ページについて、Blob からデータを取得するように `app/members/[id]/page.tsx` のデータ取得部分を変更する。

> **推奨理由:** Blob 移行後、同期スクリプトは Blob のみを更新するため、SSG で生成された詳細ページは古いデータのまま固定される。一覧ページ（`/api/members` 経由でリアルタイムデータ）と詳細ページのデータが食い違う状態を防ぐため、フェーズ 1〜4 と合わせて対応することを推奨する。

ビルド時は `data/members.json` を引き続き使用し、ランタイム（API）のみ Blob を参照する **ハイブリッド方式** も可能。この場合、Blob 更新後に Vercel の `revalidatePath` でキャッシュを無効化する。

**推奨実装:**
- **全ページ再検証（対象IDが不明な場合）:** `PUT /api/members` の処理完了後に `revalidatePath('/members/[id]', 'page')` を呼び出す（Next.js 14+、該当するすべての `/members/*` ページを再検証）
- **単一ページ再検証（対象IDが判明している場合）:** `` revalidatePath(`/members/${updatedMember.id}`) `` を呼び出す（更新対象のページのみを再検証するため効率的）
- または `app/members/[id]/page.tsx` に `export const revalidate = 3600` (ISR) を設定し、1 時間ごとに自動再生成する

---

## 移行後のアーキテクチャ

```
┌─────────────────────────────────────────────┐
│ GitHub リポジトリ                            │
│  data/members.json  （ビルド用・参照のみ）  │
│                                             │
│  GitHub Actions sync-members.yml            │
│    → scripts/sync-status.ts                 │
│      → @vercel/blob put()                   │
└────────────────────────┬────────────────────┘
                         │ (Blob 書き込み)
                         ▼
              ┌─────────────────┐
              │  Vercel Blob    │
              │  members.json   │
              └────────┬────────┘
                       │ (読み書き)
              ┌────────▼────────────┐
              │  Vercel デプロイ    │
              │  /api/members (GET) │ ✅
              │  /api/members (PUT) │ ✅
              └─────────────────────┘
```

---

## 想定作業量

| フェーズ | 作業内容 | 難易度 | 優先度 |
|---------|---------|--------|--------|
| 1 | Vercel Blob 設定・パッケージ追加 | 低 | 必須 |
| 2 | API Route 移行 | 中 | 必須 |
| 3 | 同期スクリプト移行 | 低 | 必須 |
| 4 | 初期データ移行スクリプト実行 | 低 | 必須 |
| 5 | 静的ビルド対応（SSG + revalidate） | 高 | 推奨 |

フェーズ 1〜4 の実装は 1 日以内での完了を見込む。

---

## 参考リンク

- [Vercel Blob ドキュメント](https://vercel.com/docs/storage/vercel-blob)
- [@vercel/blob SDK リファレンス](https://vercel.com/docs/storage/vercel-blob/using-blob-sdk)
- [Vercel Blob 無料枠](https://vercel.com/docs/storage/vercel-blob/limits)
