🌳 高度所要時間の目安:MVP公開まで2〜6週間(週末開発の場合)Claude Code・TypeScript・GitHub Actions

1人でSaaSのMVPをAIエージェントと作る【2026】仕様書駆動・テスト・CI・サブエージェント

AIエージェントのおかげで、1人でも「動くSaaS」は数日で形になります。問題はその先です。2人目のユーザーが来た瞬間に他人のデータが見える、予約が二重に入る、決済と権限がずれる——雰囲気で作ったMVPは、本番で最初に詰まるところがだいたい決まっています。この記事では、架空の「予約受付SaaS」を題材に、仕様書駆動 → 技術選定をADRで記録 → TDD → CI → サブエージェントで定型化 → セキュリティ → デプロイ → 運用を1本の流れで解説します。軸は一貫して「AIに任せる作業」と「人が握る判断」の切り分けです。

🔬
調査・編集:AIエージェント研究所 編集部
運営=広告運用・生成AIエージェント導入支援の現役コンサル。このサイト自体も、AIエージェントと一緒に作り・公開・運用しています。

自社サイトの運用で使っている「機械チェックで止める」「PRで人が見る」原則を、SaaS開発に一般化して解説します。→ 運営者・編集方針について

公開:2026年9月26日/最終更新:2026年9月26日
この記事の結論(要点まとめ)
  • 最初に作るのはコードではなく仕様書(PRD・ユーザーストーリー・受け入れ条件)。AIは壁打ち相手と清書係、「何を作らないか」を決めるのは人
  • 技術選定はAIに比較表を作らせ、人が決め、ADR(設計判断の記録)に残す。後からAIが勝手に方針を変えない錨になる
  • 実装は受け入れ条件→テスト→実装の順(TDD)。テストが「完了の定義」になり、AIが自己検証できる
  • GitHub Actionsでlint/typecheck/test/buildを必須に、PRで人が見てマージ。サブエージェントやSkillsで定型作業を固定化
  • 人が握るべきはスコープ・データモデル・認証認可・課金。バイブコーディングのMVPが詰まるのはだいたいこの4つ

完成イメージ:架空の予約受付SaaS「ヨヤクル」

題材は架空の予約受付SaaS「ヨヤクル」です(実在のサービスではありません。説明のための例です)。美容室・整体院・個人教室のような小規模店舗が、自分の店の予約ページを持ち、お客さんがスマホから空き枠を選んで予約できる——というよくある形を想定します。

店舗オーナーお客さん会員登録なしヨヤクル(架空のSaaS)店舗の設定メニュー・営業時間公開予約ページ/s/店舗ID確認メールキャンセル用リンク管理画面一覧・キャンセル月額プランで機能解放登録空き枠予約予約が入る一覧に送信店舗にも確認メール操作
店舗オーナーがサインアップして店舗・メニュー・スタッフ・営業時間を登録すると、公開予約ページ(/s/店舗ID)に空き枠が表示されます。お客さんは会員登録なしで予約でき、確認メール(キャンセル用リンク付き)がお客さんと店舗に届きます。オーナーは管理画面で予約一覧・キャンセル・手動追加ができ、月額プランで機能が解放されます。

MVPのゴールは「全部入り」ではなく、3店舗に実際に使ってもらい、お金を払う価値があるかを確かめることです。最終的なリポジトリは次のような形になります。

yoyakuru/(最終的な構成)tree
yoyakuru/
├── CLAUDE.md                    # AIと人の共通ルール
├── docs/
│   ├── prd.md                   # PRD:目的・対象ユーザー・スコープ・非スコープ
│   ├── stories.md               # ユーザーストーリーと受け入れ条件(Given/When/Then)
│   └── adr/
│       ├── 0001-web-framework.md
│       ├── 0002-database.md
│       └── 0003-auth.md
├── src/
│   ├── domain/                  # 予約の重なり判定など、フレームワーク非依存のロジック
│   ├── server/                  # DBアクセス・認可チェック(必ず tenantId で絞る)
│   └── app/                     # 画面とAPIルート
├── db/migrations/               # スキーマ変更は必ずマイグレーションで
├── tests/                       # 受け入れ条件と1対1のテスト
├── .github/workflows/ci.yml     # lint / typecheck / test / build
└── .claude/
    ├── settings.json            # permissions と hooks
    ├── agents/                  # テスト係・レビュー係・セキュリティ係
    └── skills/                  # 定型作業(新しいAPIルート追加、マイグレーション作成)

AIに任せる作業/人が握る判断

先に全体の分担表を示します。本記事の各STEPは、この表を具体化したものです。判断基準は「間違えたときに戻せるか」「間違いに誰が気づけるか」。戻しにくく、テストで検知しにくいものほど人が握ります。

領域人が握る判断AIに任せる作業理由
スコープ何を作らないか、MVPの成功条件PRDの清書、抜け漏れの指摘、ストーリーへの分解AIは足す提案は得意だが、削る判断の責任は取れない
データモデルテーブルと関係、マルチテナントの分け方、一意制約ER図の下書き、マイグレーションの記述、シードデータ公開後の変更コストが最も高い。データ移行は戻しにくい
技術選定最終決定とADRの承認選択肢の比較表、検証用の小さな試作運用するのは人。好みや得意分野もコスト要因
認証・認可誰が何を見られるか(権限表)、認証基盤の選択権限表に沿ったチェック実装とテスト漏れてもテストが通ってしまう代表格。事故の影響が大きい
課金料金体系、失敗時・解約時の扱い、返金方針決済サービスのSDK組み込み、Webhook処理の実装とテストお金と法務(特定商取引法の表記など)に直結
実装全般受け入れ条件の確定、PRのマージテスト→実装、リファクタ、UI、ドキュメントテストとCIで検証できる領域はAIの速度を最大限使う
運用障害時の判断、ユーザーへの連絡ログ調査、原因の仮説出し、修正PRユーザーとの関係・信用は人が持つ
助手アイ
助手アイ「何を作らないか」が一番上なんですね。作る方じゃなくて。
エージェント博士
エージェント博士AIがいると機能を足すコストがほぼゼロに見えるからのう。じゃが、足した機能はすべてテスト・レビュー・運用・サポートの対象になる。1人開発で一番足りないのはコードを書く手ではなく、全部を見張る人間の注意力じゃ。削る判断こそ、1人開発者の最重要スキルなんじゃよ

準備

本記事はClaude Codeで説明しますが、Cursor・Codexなど他のコーディングエージェントでも考え方は同じです。料金・プランや機能の詳細は執筆時点(2026年9月)の情報なので、公式ドキュメントで最新を確認してください。

ターミナルbash
# Claude Code をインストール(手順は執筆時点。公式ドキュメントで最新を確認)
npm install -g @anthropic-ai/claude-code

mkdir yoyakuru && cd yoyakuru
git init
claude

最初はまだ/initしません。CLAUDE.mdはリポジトリの中身を読んで作る雛形なので、仕様とADRができてから作る方が中身のあるものになります。

STEP 1|仕様書(PRD・ストーリー・受け入れ条件)をAIと作る

仕様書づくりでのAIの役割は「質問攻めにしてくれる壁打ち相手」と「清書係」です。いきなり「PRDを書いて」と頼むと、それらしい一般論で埋まった文書が出てきます。先に質問させるのがコツです。

プロンプト①prompt
小規模店舗向けの予約受付SaaSのMVPを1人で作りたい。まだコードは書かないで。
まず私に質問して、PRD(docs/prd.md)を作るための情報を集めて。質問は一度に5つまで。

最終的な PRD には次を含めて:
- 解決する課題と対象ユーザー(誰の・どんな場面の・何が不便か)
- MVP の成功条件(測れる形で)
- スコープ(やること)と「非スコープ(MVP ではやらないこと)」を同じ重さで
- 前提と未決事項
一般論で埋めず、私の回答に根拠がない項目は「未決」と書いて。

Claude Codeがしたこと:「対象は1人で営業している店舗か、スタッフ複数か」「現在の予約手段は電話か、LINEか」「事前決済は必要か」「キャンセル料は取るか」「複数店舗を持つオーナーを想定するか」といった質問を返し、回答をもとにdocs/prd.mdを作成。非スコープには例えば次が並びます。

docs/prd.md(非スコープ抜粋・架空)Markdown
## 非スコープ(MVP ではやらない)
- 事前決済・キャンセル料の徴収(店舗側の月額課金のみ扱う)
- 複数店舗の一括管理(1 アカウント = 1 店舗)
- お客さんの会員登録・ポイント
- 外部カレンダーとの双方向同期(MVP では予約確定時の .ics 添付のみ)
- 多言語対応
## 成功条件
- 3 店舗が 4 週間継続して使い、うち 1 店舗以上が有料プランへの移行に同意する

次にユーザーストーリーと受け入れ条件です。受け入れ条件はそのままテストになる粒度(Given/When/Then)で書かせます。これが後のTDDの入力になります。

プロンプト②prompt
docs/prd.md のスコープを、ユーザーストーリーに分解して docs/stories.md に書いて。
- 形式: 「〔役割〕として、〔目的〕のために、〔機能〕したい」
- 各ストーリーに受け入れ条件を Given/When/Then で。正常系だけでなく、
  境界値(営業時間の端、同時刻の予約)、権限(他店舗のデータ)、異常系を必ず含める
- 優先度 Must / Should / Could を付けて、Must だけで MVP が成立するか確認して
docs/stories.md(抜粋・架空)Markdown
### S-03 お客さんとして、空き枠から予約したい(Must)
- AC1: Given スタッフAの 10:00-11:00 に確定予約がある
       When 別のお客さんが スタッフA 10:30-11:30 を予約しようとする
       Then 予約は作成されず「この時間は埋まっています」と表示される
- AC2: Given スタッフAの 10:00-11:00 に確定予約がある
       When 11:00-12:00 を予約する
       Then 予約できる(終了時刻と開始時刻が同じなら重ならない)
- AC3: Given 2人がほぼ同時に同じ枠を予約する
       Then 確定するのは1件だけで、もう1人にはエラーが返る
### S-07 店舗オーナーとして、自分の店の予約だけを見たい(Must)
- AC1: Given 店舗Xのオーナーがログインしている
       When 店舗Yの予約IDを指定して詳細を開く
       Then 404 を返す(存在自体を明かさない)
判断基準:仕様書にどこまで書くか 1人開発で重い仕様書は続きません。目安は「AIが読んで迷わない」「3週間後の自分が読んで思い出せる」の2つを満たす最小限。一方で、受け入れ条件の境界値・権限・同時実行だけは省略しない。ここを書かないと、AIは正常系だけ動くコードで「完了」と言います。

STEP 2|技術選定をAIに比較させ、人が決めてADRに残す

技術選定はAIに比較させ、決定は人がします。比較の軸を指定しないと、AIは流行りの構成を無難に勧めがちです。自分の制約(1人・週末開発・得意言語・予算)を渡します。

プロンプト③prompt
docs/prd.md と docs/stories.md を読んで、ヨヤクルの技術構成の選択肢を3案比較して。
前提: 開発・運用は私1人、TypeScript が得意、月額インフラ費はMVP期間は最小に、
      予約の二重登録をDBレベルで防ぎたい、店舗ごとのデータ分離が必須。
比較の軸: 学習コスト / 1人での運用負荷 / 二重予約防止の実現方法 / マルチテナントの分離方法 /
          認証の作り方 / コストの増え方 / ロックイン / 撤退(移行)のしやすさ
表にしたあと、推奨とその理由、推奨しない場合に後悔しそうな点を書いて。決定は私がする。
案構成(例)強み弱み・リスク
ANext.js + PostgreSQL(マネージド)+ 認証ライブラリTSで統一。Postgresの制約(排他制約・一意制約)で二重予約をDBで防げるサーバー・DBの面倒を自分で見る範囲が広め
BNext.js + BaaS(Postgres系:認証・DB・行レベルセキュリティ込み)認証とDBが一体で速い。行レベルセキュリティでテナント分離を宣言的に書けるBaaS固有の設定・ポリシーの理解が必要。ロックインは中程度
CNoSQL系BaaS + SPA立ち上がりが最速重なり判定のような範囲条件の一意性をDBで保証しにくく、アプリ側の排他に頼りがち

決めたら、ADR(Architecture Decision Record:設計判断の記録)に残します。ADRは「何を決めたか」だけでなく「なぜ・何を捨てたか」を書く短い文書です。AIエージェントとの開発では特に重要で、後の会話でAIが「こちらの方がモダンです」と別の方針を持ち込んだとき、ADRが錨になります。

docs/adr/0002-database.mdMarkdown
# ADR 0002: データベースに PostgreSQL を使う

- 状態: 承認(2026-09-xx)
- 決定者: 人間(オーナー)。比較表の作成は Claude Code

## 背景
予約の二重登録を防ぐ必要がある(stories.md S-03 AC3)。アプリ側のチェックだけでは
同時リクエストで競合する。店舗ごとのデータ分離も必須(S-07)。

## 決定
PostgreSQL を使い、二重予約は排他制約(EXCLUDE USING gist)で DB レベルで防ぐ。
全テーブルに tenant_id を持たせ、アプリの全クエリで tenant_id を条件に含める。

## 却下した選択肢
- NoSQL 系: 時間範囲の重なりを DB で一意に保証しにくい
- アプリ側ロックのみ: 複数インスタンスで破綻する

## 結果・トレードオフ
- 良い: 同時予約の整合性を DB が保証。テストも「制約違反が起きるか」で書ける
- 悪い: gist 排他制約は拡張 btree_gist が必要。マネージド DB が対応しているか確認が必要
- 見直す条件: 店舗数が増え、テナント単位のDB分離が必要になったとき

STEP 3|リポジトリ構成とCLAUDE.md

仕様とADRが揃ったら、プロジェクトの雛形を作らせ、/initでCLAUDE.mdを生成し、人がルールを足します。

CLAUDE.md(抜粋)Markdown
# ヨヤクル 開発ルール

## 必読
- 仕様: docs/prd.md, docs/stories.md(受け入れ条件 = テストの元)
- 設計判断: docs/adr/。ADR と矛盾する変更は提案だけして実装しない

## コマンド
- npm run lint / npm run typecheck / npm test / npm run build
- DB: docker compose up -d db → npm run db:migrate

## 実装の進め方
1. 対象ストーリーの受け入れ条件をテストとして先に書き、失敗することを確認する
2. テストを通す最小の実装をする
3. lint / typecheck / test が通ったら、変更内容とテスト結果を報告する(コミットは指示があるまでしない)

## 絶対ルール
- DB アクセスは必ず src/server/ の関数経由。全クエリに tenantId 条件を付ける
- 入力は API の入口で zod で検証する。検証前の値を DB に渡さない
- 秘密情報(.env*)を読まない・出力しない。新しい環境変数は .env.example に名前だけ追加
- スキーマ変更は db/migrations/ に新しいファイルを追加。既存のマイグレーションは編集しない
- 依存パッケージの追加は理由を説明して承認を得てから

あわせて.claude/settings.jsonで権限を絞ります。考え方は「テスト・lint・gitの読み取りは自由に」「コミットは確認」「push・本番デプロイ・秘密情報はAIに渡さない」です。

.claude/settings.jsonJSON
{
  "permissions": {
    "allow": [
      "Bash(npm run lint:*)",
      "Bash(npm run typecheck:*)",
      "Bash(npm test:*)",
      "Bash(npm run build:*)",
      "Bash(git diff:*)",
      "Bash(git status)"
    ],
    "ask": ["Bash(git commit:*)", "Bash(npm install:*)", "Bash(npm run db:migrate:*)"],
    "deny": ["Read(./.env)", "Read(./.env.local)", "Read(./.env.production)", "Bash(git push:*)", "Bash(npx vercel:*)", "Bash(wrangler deploy:*)"]
  },
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "npm run lint --silent 1>&2 && npm run typecheck --silent 1>&2 || exit 2" }]
      }
    ]
  }
}

PostToolUseのhookで、ファイルを編集するたびにlintと型チェックが走り、失敗するとexit 2で結果がClaudeに返って自分で直します。テスト全体はhookに入れず、実装の区切りとCIで回します(編集のたびに数分かかると作業が極端に遅くなるため)。秘密情報のファイルは個別に列挙してdenyし、変数名だけを書いた.env.exampleはAIが読めるようにしておくと、必要な環境変数をAIが把握できます(Read(./.env.*)のようにまとめてdenyすると.env.exampleも読めなくなる点に注意)。hooksとpermissionsの記法は執筆時点の公式ドキュメントに基づくため、導入時に確認を。

STEP 4|テスト先行(TDD)でAIに実装させる

AIエージェントとTDDは相性が抜群です。テストが先にあると、「完了」の定義が機械的に決まり、AIが自分で検証→修正のループを回せるからです。受け入れ条件S-03を例にします。

プロンプト④prompt
docs/stories.md の S-03 を実装したい。まずテストだけ書いて、実装はまだしないで。
- AC1, AC2 は src/domain/ の純粋関数(重なり判定)の単体テストで
- AC3(同時予約)は DB の排他制約が効くことを確認する統合テストで
- テストを実行して、すべて「期待どおりに失敗する」ことを確認して報告して
tests/domain/overlap.test.tsTypeScript
import { describe, it, expect } from "vitest";
import { overlaps } from "../../src/domain/overlap";

const t = (hhmm: string) => new Date(`2026-10-01T${hhmm}:00+09:00`);

describe("overlaps(S-03 AC1/AC2)", () => {
  it("一部でも重なる枠は重複とみなす", () => {
    expect(overlaps({ start: t("10:00"), end: t("11:00") }, { start: t("10:30"), end: t("11:30") })).toBe(true);
  });
  it("終了時刻と開始時刻が同じなら重ならない(半開区間)", () => {
    expect(overlaps({ start: t("10:00"), end: t("11:00") }, { start: t("11:00"), end: t("12:00") })).toBe(false);
  });
  it("一方が他方を完全に含む場合も重複", () => {
    expect(overlaps({ start: t("10:00"), end: t("12:00") }, { start: t("10:30"), end: t("11:00") })).toBe(true);
  });
});

テストが失敗するのを確認してから「テストを通す最小の実装をして。テストは変更しないで」と頼むと、AIは次のような実装を書きます。

src/domain/overlap.tsTypeScript
export type Slot = { start: Date; end: Date };

// 半開区間 [start, end) として重なりを判定する。
// 「11:00 終了」と「11:00 開始」は重ならない(S-03 AC2)
export function overlaps(a: Slot, b: Slot): boolean {
  return a.start < b.end && b.start < a.end;
}

ただし、AC3(同時予約)はこの関数だけでは守れません。2つのリクエストが同時に「空いている」と判定して両方INSERTすれば二重予約になります。ここでADR 0002の決定が効きます。

db/migrations/0003_no_double_booking.sqlSQL
-- 同じスタッフの確定予約が時間的に重なることを DB で禁止する(S-03 AC3 / ADR 0002)
CREATE EXTENSION IF NOT EXISTS btree_gist;

ALTER TABLE reservations
  ADD CONSTRAINT no_double_booking
  EXCLUDE USING gist (
    staff_id WITH =,
    tstzrange(start_at, end_at, '[)') WITH &&
  )
  WHERE (status = 'confirmed');

'[)'は半開区間の指定で、ドメイン関数の判定と一致させています。アプリ側では制約違反のエラー(PostgreSQLのエラーコード23P01=exclusion_violation)を捕まえて「この時間は埋まっています」に変換します。アプリの事前チェックはUXのため、DB制約は整合性のため、と役割を分けるのが判断のポイントです。

AIのTDDで起きがちなこと
  • テストと実装を同時に書く:テストが実装に合わせて書かれ、仕様の確認にならない。「まずテストだけ・失敗を確認」を明示する
  • テストが通らないとテストを直す:CLAUDE.mdで禁止し、差分レビューでテストファイルの変更を最初に見る(詳しくは既存コードの改修・リファクタリング編の「差分レビューの観点」)
  • モックだらけの統合テスト:DB制約のテストをモックで書くと何も検証していない。制約系は本物のDB(Docker等)で回す

STEP 5|CI(GitHub Actions)で機械チェックを必須にする

手元のhooksは「AIが作業中に気づく」ための仕組み、CIは「誰が作業しても、マージ前に必ず通る関門」です。1人開発こそ、自分の注意力に頼らない関門が必要です。

.github/workflows/ci.ymlYAML
name: ci
on:
  pull_request:
  push:
    branches: [main]

jobs:
  check:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
        ports: ["5432:5432"]
        options: >-
          --health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10
    env:
      DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm run db:migrate
      - run: npm test
      - run: npm run build
      - run: npm audit --audit-level=high

GitHubのブランチ保護(ルールセット)で「このチェックが通らないとmainにマージできない」設定にします。これで、AIが「完了しました」と言っても、CIが緑でなければ完了ではないという状態になります。

さらにPRレビューの一次チェックをAIに任せたい場合、GitHub Actions用のanthropics/claude-code-actionで、PRやIssueで@claudeとメンションしてレビューや修正を依頼する運用もできます(Claude Codeで/install-github-appを実行すると設定を案内してくれます。設定方法は執筆時点の情報なので公式READMEで確認してください)。ただしAIレビューは人のレビューの代わりではなく前処理です。マージボタンは人が押します。

判断基準:CIに何を入れるか 入れる基準は「落ちたら必ず直すもの」だけ。警告を垂れ流すチェックを入れると、人もAIも赤に慣れて無視し始めます。依存パッケージの脆弱性チェック(npm audit)もhigh以上に絞る、E2Eテストは重いならmainへのpush時だけにする、といった調整をします。

STEP 6|サブエージェント・Skillsで定型作業化

開発が進むと、「新しいAPIルートを追加する」「マイグレーションを作る」「PR前にレビューする」といった毎回同じ手順の作業が出てきます。毎回プロンプトで説明する代わりに、サブエージェントやSkillsに固定化します。

仕組み置き場所向いている用途
サブエージェント.claude/agents/<name>.md独立した文脈・限定したツールで動かしたい役割(テスト係、レビュー係、セキュリティ係)
Skills.claude/skills/<name>/SKILL.md手順書のパッケージ化。必要なときにClaudeが読み込む(新しいAPIルートの追加手順、リリース手順)
カスタムスラッシュコマンドプロジェクトの設定ディレクトリ(形式は公式ドキュメント参照)自分で明示的に呼ぶ定型プロンプト
.claude/agents/security-reviewer.mdMarkdown
---
name: security-reviewer
description: PR前に差分をセキュリティ観点でレビューする。認証・認可・入力検証・秘密情報・依存パッケージを確認。コードは修正しない。
tools: Read, Grep, Glob, Bash
---
あなたはマルチテナントSaaSのセキュリティレビュアーです。`git diff main...HEAD` を対象に確認し、
「重大 / 要確認 / 軽微」に分けてファイルと行を添えて報告してください。修正はしません。

- 認可: src/server/ の全クエリに tenantId 条件があるか。ID を URL から受け取る箇所で所有者チェックがあるか
- 認証: 管理画面の API ルートにセッション確認があるか。公開予約ページから管理 API を呼べないか
- 入力検証: API の入口で zod 検証しているか。検証前の値が DB・HTML・メール本文に渡っていないか
- 秘密情報: キー・トークンのハードコード、ログへの個人情報(メール・電話)出力
- 依存: 新規追加パッケージの必要性・メンテ状況・権限(postinstall スクリプト等)
.claude/skills/add-api-route/SKILL.mdMarkdown
---
name: add-api-route
description: ヨヤクルに新しい API ルートを追加するときの手順。管理画面・公開ページどちらの API にも使う。
---
1. docs/stories.md の該当ストーリーと受け入れ条件を確認する
2. 入力スキーマを zod で src/app/api/<name>/schema.ts に定義する
3. 受け入れ条件のテストを tests/api/<name>.test.ts に先に書き、失敗を確認する
   - 必ず含める: 未ログイン→401、他テナントのID→404、不正入力→400
4. src/server/ にテナントで絞ったデータアクセス関数を追加する(直接 DB を呼ばない)
5. ルートを実装し、npm run lint / typecheck / test を通す
6. 変更ファイル一覧とテスト結果を報告する

判断基準は「3回同じ説明をしたら固定化する」。最初から作り込むと、実態に合わない手順書をAIが律儀に守る、という逆効果もあります。またSkillsの手順を変えたら、それ自体もPRでレビューします。手順書は“AIへの権限”と同じくらい影響が大きいからです。

助手アイ
助手アイテストに「他テナントのID→404」を必ず入れる、って手順書に書いておくのがいいですね。人間でも忘れそう……。
エージェント博士
エージェント博士そのとおりじゃ。「忘れそうなこと」ほど手順とテストに埋め込む。記憶や注意力に頼るルールは、人間でもAIでもいつか破られる。仕組みにしたルールだけが残るんじゃよ

STEP 7|コードレビューとセキュリティの観点

コードレビューの観点

  • 受け入れ条件との対応:PRの説明に「どのストーリーのどのACを満たしたか」を書かせ、テスト名と突き合わせる
  • テストの差分を先に読む:弱められたassert、追加されたskip、実装に寄せた期待値がないか
  • スコープ:依頼外のファイル変更・依存追加がないか(git diff --stat)
  • ADRとの整合:別のライブラリや別の設計を持ち込んでいないか
  • 読めるか:3週間後の自分が読めない抽象化は、AIが書いても人が書いても負債

セキュリティの観点(MVPでも省略しない4点)

領域MVPでの最低ラインAIがやりがちな穴
認証自作せず、実績のある認証ライブラリ/BaaSの認証を使う。パスワードを自前でハッシュ化・保存しないJWTを自作し署名検証を省く、セッションの有効期限なし
認可権限表を作り、全データアクセスをテナントで絞る。「他テナントのID→404」をテストで保証画面では隠しているがAPIを直接叩けば見える(IDOR:推測可能なIDで他人のデータにアクセスできる脆弱性)
入力検証APIの入口でスキーマ検証。出力時はフレームワークのエスケープに任せるクライアント側だけで検証、生のHTML挿入、文字列連結のSQL
依存パッケージロックファイルをコミット、npm auditをCIに。追加は承認制似た名前の別パッケージ、存在しないパッケージ名の提案(実在確認が必要)、不要な大型依存

入力検証の例です。APIの入口で検証し、検証済みの値だけを内側に渡します。

src/app/api/reservations/schema.tsTypeScript
import { z } from "zod";

export const CreateReservation = z.object({
  staffId: z.string().uuid(),
  menuId: z.string().uuid(),
  startAt: z.string().datetime({ offset: true }),
  customerName: z.string().trim().min(1).max(50),
  customerEmail: z.string().email().max(254),
  note: z.string().max(500).optional(),
});

export type CreateReservationInput = z.infer<typeof CreateReservation>;

// ルート側: const parsed = CreateReservation.safeParse(await req.json());
//           if (!parsed.success) return Response.json({ error: "invalid_input" }, { status: 400 });

認可はテストで保証します。「他店舗の予約IDを指定しても取れない」ことを、実装ではなくテストで確認するのがポイントです。

tests/server/reservations.authz.test.tsTypeScript
import { describe, it, expect } from "vitest";
import { getReservation } from "../../src/server/reservations";
import { seedTwoTenants } from "../helpers/seed";

describe("S-07 AC1: 他店舗の予約は見えない", () => {
  it("店舗Xのオーナーが店舗Yの予約IDを指定すると null(APIでは404)", async () => {
    const { tenantX, reservationOfY } = await seedTwoTenants();
    const result = await getReservation({ tenantId: tenantX.id }, reservationOfY.id);
    expect(result).toBeNull();
  });
});

課金(例:Stripe等の決済サービスでのサブスクリプション)は、MVPでも料金体系・支払い失敗時の扱い・解約時のデータ・特定商取引法に基づく表記を人が決めます。実装面では、決済状態の正はWebhookで受け取り、Webhookの署名を必ず検証し、同じイベントが複数回届いても二重処理しない(冪等にする)——この3点をテストで確認します。外部サービス連携の詳細はAIエージェントで開発してみよう|API連携編の各記事を参照してください。

STEP 8|デプロイと運用(ログ・エラー監視)

デプロイ:本番を触る権限はAIに渡さない

Vercel・Cloudflare・各種PaaSなど、GitHub連携でmainへのマージ時に自動デプロイされる構成が1人開発では楽です。こうしておくと「デプロイ=PRをマージすること」になり、AIに本番デプロイの権限を渡す必要がありません(上のsettings.jsonでデプロイ系コマンドをdenyしているのはそのため)。

  • 環境を分ける:本番・プレビュー(PRごと)・ローカル。本番DBの接続情報はデプロイ先の環境変数にだけ置き、ローカルやAIの作業環境には置かない
  • マイグレーションは前方互換に:列の削除や改名は「追加→両方に書く→移行→削除」の段階で。デプロイとマイグレーションの順序で壊れない形に
  • ロールバック手順を先に確認:1つ前のデプロイに戻す操作を、公開前に一度やっておく

運用:ログとエラー監視

  • 構造化ログ:JSONでtenantId・リクエストID・処理名を出す。ただしメールアドレス・電話番号などの個人情報はログに出さない
  • エラー監視:Sentry等のエラー監視サービスで例外を集約し、新種のエラーだけ通知する(全部通知すると見なくなる)
  • 死活監視:公開予約ページとヘルスチェック用エンドポイントを外形監視
  • AIの出番:エラーのスタックトレースとログを渡して「原因の仮説を3つ、確かめ方とセットで」と頼み、再現テストを書かせてから修正PRを作らせる。再現テストが先なのは、修正の正しさを機械で確かめ、同じバグの再発も防ぐため
プロンプト(障害対応)prompt
本番で次のエラーが出ている(スタックトレースとログを下に貼る)。コードはまだ変えないで。
1. 原因の仮説を 3 つ、それぞれの確かめ方とセットで出して
2. 最も可能性が高い仮説について、このエラーを再現する失敗テストを書いて実行して
3. 再現できたら、修正方針を提案して(実装は承認後)
ログに個人情報が含まれていたら、その箇所も指摘して
--- ログ ---
(ここに貼る。メールアドレスなどはマスクしてから)

バイブコーディングのMVPが本番で詰まる典型例と回避策

「バイブコーディング(vibe coding)」は、2025年2月にAndrej Karpathy氏が広めた言葉で、コードの中身を細かく見ずにAIとの会話の“雰囲気”で作り進めるスタイルを指します。試作や個人の遊びには強力ですが、他人のデータとお金を預かるSaaSを雰囲気で任せきりにすると、次のような詰まり方をします。本シリーズが推すのは、仕様を伝え、機械と人で確認する進め方です。

典型的な詰まり何が起きるか回避策(本記事のどこで防ぐか)
テナント分離の漏れURLのIDを変えると他店舗の予約が見える権限表+全クエリをテナントで絞る関数経由+「他テナント→404」テスト(STEP 3, 7)
同時実行の競合同じ枠に2件入る。在庫・予約・残高系で頻発受け入れ条件に同時実行を書く+DB制約(STEP 1, 4)
データモデルの作り直し「1アカウント=1店舗」前提で作り、後から複数店舗対応で全面改修非スコープを明記しつつ、将来の拡張で壊れないキー設計を人が決める(STEP 1, 2)
秘密情報の露出APIキーがフロントのコードやGitに入る.envをdeny・.gitignore、環境変数はサーバー側のみ(STEP 3, 8)
直すと別の所が壊れるテストがなく、修正のたびに手動確認。やがて誰も触れなくなる受け入れ条件=テスト、CI必須(STEP 4, 5)
本人も構造を説明できない障害時にAIの回答を検証できないADRとdocsを残す、PRで差分を読む習慣(STEP 2, 7)
課金と権限のずれ解約したのに有料機能が使える/払ったのに解放されない決済Webhookを正とし、署名検証と冪等性をテスト(STEP 7)

逆に言えば、画面の見た目や文言、管理画面の細かいUIはバイブコーディング的に高速で回してかまいません。間違えてもすぐ気づけて、すぐ戻せるからです。「雰囲気で任せてよい領域」と「仕様とテストで縛る領域」を分けることが、1人開発のスピードと安全の両立になります。

事例:このサイトの運用で学んだ原則

自社事例

「機械チェックで止める」「PRで人が見る」をSaaS開発に一般化する

このサイト(HTML+CSSのみの静的サイト、約57ページ)は、Claude Codeとの共同作業で作成・運用しています。SaaSではありませんが、AIエージェントに継続的に変更を任せる中で固まった原則は、そのままMVP開発にも当てはまります。

  • 機械チェックで止める:共通フッター(アフィリエイト表記を含む)を全ページに流し込むbuild.jsは、表記を入れられないページが1つでもあるとexit 1で止まります。景表法まわりを検査するkeihyo-check.jsも、重大な違反があればexit 1。「気をつける」ではなく「通らなければ進めない」——SaaSでいえば、受け入れ条件のテストとCIの必須チェックに当たります
  • PRで人が見る:AIは作業ブランチで変更し、人が差分を確認してマージします。全ページのヘッダー統一のような一括変更も同じ流れで、変更ファイル数が想定どおりかも確認します。SaaSでいえば、マージ権限とデプロイ権限を人が持つ構成です
  • 秘密情報はAIとGitの外に:.gitignoreで.env・鍵ファイル・credentials.json等をコミット対象から外し、ビルド用スクリプトや部品ファイルは配信対象から除外しています

仕組みの詳細は【自社事例】AIエージェントでメディアサイトを運用する仕組みで公開しています。当社の導入支援の現場でも、まず「止める仕組み」と「人が見る場所」を決めてからAIに任せる範囲を広げる、という順番を基本にしています。

他社の公開事例

  • Anthropicは、社内でClaude Codeを広く使い、Claude Code自体のコードの多くをClaude Codeで書いていると公表しています
  • AnthropicのClaude 4発表(2025年5月)では、楽天がClaude Opus 4を約7時間自律的に動かしてオープンソースのリファクタリングを行った事例が紹介されました
  • 一般論として、エンジニアではない人がAIを使って業務ツールや小さなサービスを作る動きも広がっています。作る敷居が下がった分、「運用に耐えるか」を判断する知識の価値は上がっています

発展アイデア

  • E2Eテストで受け入れ条件を画面から確認:Playwright等で「予約→確認メール→キャンセル」の一連をテスト。重いのでmainへのpush時だけ実行
  • worktreeで並列開発:独立したストーリー(管理画面の一覧、公開ページのデザイン)をgit worktreeで別ディレクトリに分け、別セッションのClaude Codeで並行して進める
  • ヘッドレス実行で定期メンテ:claude -pで依存パッケージの更新PRや、エラーログの週次サマリーを定期作成(結果はPRやIssueで人が確認)
  • プロダクトにAIを組み込む:予約の問い合わせ対応などに生成AIを使いたくなったら、Claude APIでAIエージェントを作るへ

Web・サーバー・Gitなどの基礎から押さえたい方は、IT基礎ロードマップから読むのがおすすめです。

よくある質問

1人でAIエージェントとSaaSのMVPを作るのに、どれくらいの期間がかかる?

予約受付のような機能が絞られたMVPなら、週末開発でも数週間で公開まで到達できることが多いです。ただし期間を決めるのはコードを書く速度ではなく、仕様を決める時間、テストとレビューの時間、決済や認証などの設定作業です。スコープを絞るほど速くなるので、非スコープを明記することが一番の短縮策です。

仕様書やADRを書くのは、1人開発には重すぎない?

重い文書は不要ですが、最小限は書く価値があります。受け入れ条件はそのままテストになり、ADRは後の会話でAIが別の方針を持ち込んだときの錨になります。AIが読んで迷わず、数週間後の自分が読んで思い出せる量を目安にしてください。

AIに任せてはいけない判断は何?

何を作らないか(スコープ)、データモデル、認証・認可の方針、課金の3〜4点は人が握るべきです。いずれも公開後に変更するコストが高く、テストが通っていても間違いに気づきにくい領域です。実装そのものは、受け入れ条件のテストとCIで検証できる範囲でAIに任せて構いません。

バイブコーディングで作ったMVPは作り直すしかない?

作り直しが必要とは限りません。まず現状の振る舞いをテストで固定し、テナント分離、同時実行、秘密情報、認可の4点から優先して点検・修正するのが現実的です。既存コードをテストで囲ってから直す手順は、既存コードの改修・リファクタリング編で詳しく解説しています。

まとめ|AIで速く作り、仕組みで安全に出す

1人でSaaSのMVPを作るとき、AIエージェントは「手が10本ある開発チーム」になってくれます。ただしそのチームには方針を決める人と、品質の関門が必要です。仕様書と受け入れ条件で「何を作るか・何を作らないか」を決め、ADRで設計判断を固定し、TDDで完了の定義を機械にし、CIとPRで人の確認を必ず通す。そして、スコープ・データモデル・認証認可・課金は人が握る。

雰囲気で速く回してよい領域と、仕様とテストで縛る領域を分ければ、1人開発のスピードを落とさずに、2人目・3人目のユーザーを迎えられるMVPになります。外部サービス(Slack・Notion・Stripe・GitHubなど)とつなぐ実装は、次のAIエージェントで開発してみよう|API連携編で分野別に解説しています。

作れるようになったら、スキルを収入につなげる

AIエージェントで「作れる」ことは、それ自体が仕事になります。案件獲得や転職、作業環境づくりに役立つ記事をまとめました。

AIで独立するには?未経験から案件を取る6ステップ【2026】自動化・開発スキルを案件にする。フリーランスエージェントの選び方も
記事を読む

次の一歩

姉妹サイトの記事で、次に気になりやすいことを調べられます。