2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

2
Last updated at Posted at 2026-05-03

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 営業のデモまで通した話。
保存しといて、明日からの自分に見せて。

明日からコピペで使えるチェックリスト:5日でSaaS本番リリース

Day 1:MVP スコープ確定

  • 作らない機能を先にリストアップ(認証以外で迷ったら全部 v2 行き)
  • 1 つのコアフロー(誰が→何を入れて→何が返るか)を 1 行で書く
  • 競合 3 つの料金ページをスクショして並べる(自分の価格帯を即決)
  • DB スキーマは users / 1 entity / events の 3 テーブルから始める

Day 2:技術選定(Claude Code 前提)

  • Next.js App Router + TypeScript(迷うな)
  • Supabase(auth + DB + storage が 1 回設定で全部入る)
  • Vercel(push 即 deploy、Preview URL を営業ツールにする)
  • .env.local を最初に固める。秘匿値は本番 Vercel に直接登録

Day 3:実装の地雷ポイント

  • webhook の raw bodyreq.text() で読む(req.json() は署名検証で必ず落ちる)
  • Server Actions より API Route の方が retry / debug しやすい
  • NEXT_PUBLIC_* を動的参照(process.env[key])すると client で消える → 直接参照
  • Supabase RLS は 最初から ON。あとで入れるとデータ整合性が壊れる

Day 4:本番デプロイ

  • Vercel commit author = GitHub verifiable email(Pro 必須要件)
  • Supabase Free Tier の active project は owner 単位 2 個まで(既存に prefix で間借り)
  • env corruption (eyJ2IjoidjIi…) が出たら CLI で env 再追加
  • 本番 URL を /health で死活監視(Vercel Cron 1 分間隔)

Day 5:営業可能な状態を作る

  • LP に「3 行説明 + 30 秒デモ動画 + 申込フォーム」だけ置く
  • 利用規約 / プライバシーポリシー / 特商法 の 3 点を生成 AI に法人情報渡して即作成
  • DM 営業用テンプレ 5 種を準備(業種別)
  • 解約導線を最初から付ける(後付けすると審査で詰まる)

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

🎬 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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?