🚀

Build Log #1:不動産業者向けSaaSをClaude Codeで5日で本番リリースした全工程

に公開

Claude Code で不動産業者向けの動画自動生成 SaaS を 5 日で本番リリースした。
ゼロから着手して、5 日目の夜には誰でもサインアップできて、Stripe Checkout で月額が落ちて、AI が台本とナレーションと画像を作って MP4 に組み上げて配信するところまで通った。

俺はコードをほとんど自分で書いてない。書いたのは Claude Code。俺がやったのは、機能の取捨選択と、詰まったときの方向決めと、本番で壊れたところを再現して指示し直すこと。

このシリーズは Build Log として、俺が運用してる SaaS の開発ログを全部書く。失敗も凍結も地雷も全部。最初の数本は完全無料で出す。先に断っておくと、この記事は読み終わった瞬間に「次プロジェクトの初日チェックリスト」として保存できる作りにしてある。

note 版:mintototo1


数字パネル(このプロジェクト単体)

  • 着手 → 本番デプロイ:5 日
  • 1 日あたりの作業時間:6〜10 時間
  • 自分でキーボード叩いた時間の比率:約 15%(残り 85% は Claude Code に指示)
  • 月運用コスト:約 ¥20,000(Vercel Pro / Supabase / 各種 API)
  • 本人スキル:非エンジニア
  • 初月売上:¥0(プロダクト起こしてから B2B 営業は別レーンで回す方針)

数字を盛ってない。盛る価値がない。盛らない方が信用される。


Day 0:「やらないこと」を先に決める

着手前に紙に 20 機能書き出して、5 機能まで削った。残したのは:

  1. メアドサインアップ
  2. 物件情報フォーム
  3. AI が台本 + ナレーション + 画像を生成
  4. MP4 を組み立てて配信
  5. Stripe で月額課金

ダッシュボード、チーム、分析、招待コード、解約フロー、メール通知。全部後回し。
特に解約フローを切ったのが効いた。Stripe Customer Portal に丸投げ。自前 UI ゼロ。

学び:5 日でリリースするなら、機能を削るのが一番効く。「これは後でいい」と言える勇気が、5 日と 20 日を分ける。


Day 1:認証と DB スキーマ

Supabase 立てて auth 有効化。テーブルは 4 つだけ。

-- profiles: ユーザー
-- projects: 生成案件
-- assets: 生成成果物(R2 URL)
-- subscriptions: Stripeステータスのキャッシュ

ここで 1 個目の地雷。Supabase の Free Tier は「同一オーナー配下で同時に active にできるプロジェクトが 2 つまで」。俺はすでに別プロダクトで 2 枠使ってた。

新プロジェクト作りに行ったら作れない。30 分溶かして気づいた。

解決:別プロダクトを止めるんじゃなくて、既存プロジェクトに prefix 付きスキーマで間借りする運用に切り替えた。

-- 既存プロジェクトの中で
create schema vt_realestate;
-- このスキーマの中で profiles / projects / assets / subscriptions を切る

リポは別、DB スキーマだけ間借り。これで以後、新プロダクトを起こすたびに新 Supabase 立てる必要がなくなった。今でもこの運用が続いてる。

学び:無料枠は最初に「枠と運用ルール」を決めとく。後で詰むと判断ミスをパニックでやる羽目になる。


Day 2:生成パイプライン(一番怖かった日)

台本生成(Anthropic API)→ ナレーション(音声生成)→ 画像生成(fal)→ MP4 合成(FFmpeg)。
直列で 4 本の API を叩くとユーザーは 8〜10 分待たされる。これでは課金してくれない。

設計:

  • 受付 API は DB に job を insert して即 200 を返す
  • 実処理は Worker が裏で回す
  • フロントは status を 5 秒間隔でポーリング
// /api/projects/route.ts (受付だけ)
export async function POST(req: Request) {
  const body = await req.json();
  const { data, error } = await supabase
    .from('projects')
    .insert({ user_id, status: 'queued', input: body })
    .select().single();
  if (error) return new Response(error.message, { status: 500 });
  return Response.json({ id: data.id });
}

これで「動くプロダクトの体感」を先に作って、内部の遅さを後回しにした。体感が崩れなければ、内部最適化は後でいくらでも効く。

ここでの地雷:fal の動画生成モデル名を推測でハードコードしたら本番で 404 連発。endpoint も推測で書いたらそれも違って合計 2 時間溶かした。

// ❌ こうやってた
await fetch('https://fal.run/fal-ai/wan-2.2-video', { /* ... */ });
// モデル名が違う、endpointも違う、ドメインまで違ってた

// ✅ 必ずやる手順
// 1. 公式ドキュメントで現在の正式エンドポイントを確認
// 2. curl で1回 200 を取る
// 3. そのレスポンスJSONを手元に保存してから本番コードに入れる

学び:外部 API のモデル名・endpoint は絶対に推測で書かない。「成功した 1 本のレスポンス」が手元にあるまで本番に入れない。これは今、俺の全プロダクトでルール化してる。


Day 3:Stripe 課金と Webhook(Next.js App Router の罠)

Stripe の基本構造は素直。Product と Price を月額で 1 本作って、Checkout Session を切る API を生やすだけ。
詰まったのは Webhook。subscription.created / updated / deleted を受けて DB に反映する。

地雷:Next.js App Router で Stripe webhook の署名検証は、body を raw string のまま渡さないと壊れる。req.json() でパースしちゃうと署名が合わなくなる。

// ❌ これは死ぬ
const body = await req.json();
const event = stripe.webhooks.constructEvent(JSON.stringify(body), sig, secret);
// → "No signatures found matching the expected signature for payload"
// ✅ 正しい
// /api/webhooks/stripe/route.ts
export async function POST(req: Request) {
  const sig = req.headers.get('stripe-signature')!;
  const body = await req.text(); // ← raw string で取る、ここがキモ
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (e) {
    return new Response('Invalid signature', { status: 400 });
  }
  // event.type で分岐して subscriptions テーブル更新
  return Response.json({ received: true });
}

ローカルでは動くのに本番でだけ 400 が出る。Stripe CLI で stripe listen しながら手元で再現するまで気づけない。

学び:Stripe webhook は raw body で署名検証。Next.js App Router では req.text() を使う。これを忘れると本番でだけ壊れる。


Day 4:Vercel デプロイの env 地雷 2 連発

UI を LP まで含めて雑に組んで、本番に push した。動かない。

地雷①:Vercel env がある日突然壊れた

Project Settings の env を CLI で更新したあと、UI で見ると正常に見えるのに本番で「Missing env」が出続ける。
中身を覗くと eyJ2IjoidjIi... で始まる 1320 文字の謎の文字列で固定されてた。Vercel 側の envelope(暗号化された保存形式)が壊れてた。

解決:

# 該当envを一回完全に消す
vercel env rm STRIPE_SECRET_KEY production

# もう一度入れ直す(ここで復旧する)
vercel env add STRIPE_SECRET_KEY production

UI 上では完全に正常に見えるので、原因に当たりがつくまで 30 分溶かした。

地雷②:NEXT_PUBLIC_ の動的参照禁止

クライアント側で env を扱う共通ヘルパーを書いて、process.env[key] と動的参照させてた。

// ❌ これは Next.js のビルドが inline できない
function getEnv(key: string) {
  return process.env[key];
}
const url = getEnv('NEXT_PUBLIC_SUPABASE_URL'); // → undefined on client

NEXT_PUBLIC_ プレフィックスの env は、ビルド時に process.env.NEXT_PUBLIC_FOO という直接の文字列リテラルとして書いてある場所だけ inline される。動的 lookup だとビルド時に置換されず、クライアントで undefined になる。

// ✅ 直接書く
const url = process.env.NEXT_PUBLIC_SUPABASE_URL!;

ローカル開発ではどっちでも動く(Node.js 側で参照できるから)。だから本番デプロイで初めて壊れる。

学び:NEXT_PUBLIC_ は直接参照する。動的 lookup ヘルパー禁止。


Day 5:本番リリースと最後の関門

Vercel に本番デプロイ。…通らない。

エラー:401。デプロイ自体が始まらない。
原因:commit author の email が GitHub-verifiable じゃないアドレスになってた。Vercel Pro はチームのメンバー seat と GitHub 上の verified email を照合してて、合わないと毎 deploy 401 を返す。

解決:

# GitHub の noreply email に切り替える
git config --local user.email "12345678+yourname@users.noreply.github.com"
git commit --amend --reset-author --no-edit
git push --force-with-lease

これで通った。

その後、最初の社内テストでサインアップ → Checkout → 生成 → 配信が通って、5 日目の夜 23 時に本番リリース。


結果(Day 5 終了時点)

  • 本番 URL で誰でもサインアップできる
  • Stripe Checkout で月額が落ちる
  • AI が動画を生成して配信する
  • ダッシュボードで履歴が見れる
  • 売上:¥0(営業はこれから別レーンで回す)

売上 0 は織り込み済み。プロダクトを 5 日で出す目的は「売れるかを検証できる状態」を作ること。営業は別の戦術。


確定ルール(保存推奨。次プロジェクトの初日チェックリスト)

  1. 機能は最初に 5 個まで削る。20→5 の取捨選択を着手前に必ず終わらせる
  2. Supabase Free Tier の 2-project cap を最初から織り込む。新プロダクトはスキーマ間借り運用
  3. 外部 API のモデル名・endpoint は絶対に推測で書かない。1 回成功したレスポンスを手元に持つまで本番に入れない
  4. Stripe webhook は raw body 検証。Next.js App Router では req.text() を使う
  5. NEXT_PUBLIC_ は直接 process.env.NEXT_PUBLIC_FOO と書く。動的 lookup ヘルパー禁止
  6. Vercel env が壊れたら CLI で remove → add で再投入。UI だけ見て判断しない
  7. git の commit author は GitHub の noreply email に揃える。Vercel Pro でハマる
  8. 解約フローは Stripe Customer Portal に丸投げ。自前 UI 禁止(5 日では作らない)
  9. 受付 API は queue insert だけにして即 200 を返す。重い処理は Worker に逃がす
  10. ローカルで動いて本番で死ぬ系(webhook / NEXT_PUBLIC_ / env)は最初から疑う

このリストは俺自身が次プロダクトの Day 1 で見返すために書いた。コピペして自分の lessons.md に貼っとけば、5 日のうち 1 日は確実に節約できる。


このシリーズは続く。次の記事は「LINE 受付 AI を 1 日で作って即営業した話」を書く。
1 日でリリースして、その日のうちに B2B 営業のデモまで通した話。
保存しといて、明日からの自分に見せて。

俺が運営してるプロダクト

🎬 VideoTracker — 不動産業者向け動画自動生成 SaaS
動画1本¥596。問合せ倍率の想定値はシミュレーションで2.8倍(実測は検証中)。
https://komugi-ai.jp/realestate

🤖 Mint Agent — Slack で @AI に話しかけて業務代行(近日リリース)
議事録投稿・メール返信・データ集計が Slack 内で完結
→ ベータ Waitlist:https://agent.komugi-ai.jp

業務効率化・SaaS 開発相談 → X DM @mintnekoneko0
過去記事まとめ:https://note.com/mintototo1

Discussion