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 機能まで削った。残したのは:
- メアドサインアップ
- 物件情報フォーム
- AI が台本 + ナレーション + 画像を生成
- MP4 を組み立てて配信
- 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 日で出す目的は「売れるかを検証できる状態」を作ること。営業は別の戦術。
確定ルール(保存推奨。次プロジェクトの初日チェックリスト)
- 機能は最初に 5 個まで削る。20→5 の取捨選択を着手前に必ず終わらせる
- Supabase Free Tier の 2-project cap を最初から織り込む。新プロダクトはスキーマ間借り運用
- 外部 API のモデル名・endpoint は絶対に推測で書かない。1 回成功したレスポンスを手元に持つまで本番に入れない
- Stripe webhook は raw body 検証。Next.js App Router では
req.text()を使う -
NEXT_PUBLIC_は直接process.env.NEXT_PUBLIC_FOOと書く。動的 lookup ヘルパー禁止 - Vercel env が壊れたら CLI で remove → add で再投入。UI だけ見て判断しない
- git の commit author は GitHub の noreply email に揃える。Vercel Pro でハマる
- 解約フローは Stripe Customer Portal に丸投げ。自前 UI 禁止(5 日では作らない)
- 受付 API は queue insert だけにして即 200 を返す。重い処理は Worker に逃がす
- ローカルで動いて本番で死ぬ系(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 body を
req.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