はじめに: なぜこの記事を書くか
個人開発で SaaS を作ろうとすると、コードを書く時間と同じくらい「審査・法令・本番運用準備」に時間が消えていく。
Zenn の前回記事 (Claude Code が出した提案を却下する技術) では、Claude Code との設計判断ログを書いた。今回はその続編で、コード以外の戦いの判断ログを書く。
連休 4日間で、個人開発の B2C SaaS (英語学習アプリ) を:
- LP 完全実装
- Stripe 7日間無料トライアル実装
- 法的ページ3種作成 (特商法、プライバシー、利用規約)
- Stripe 本番審査申請 → 24時間で通過
- 独自ドメイン取得と Clerk Production 移行
- 本番モードでの実決済確認
まで持っていった。実際にやってみると「コード書くのは簡単な部分だった」と分かる。本記事ではそのプロセスを判断ログ形式で書く。
技術解説の記事ではなく、判断ログの記事だ。
プロジェクト概要
開発中のプロダクト
- biz-english-master (B2C 英語学習 SaaS)
- 月額 ¥2,980 (税抜) のサブスクリプション、7日間無料トライアル付き
- Next.js 16 + Clerk + Stripe + Gemini API + Vercel
- 個人事業主、1人開発、Claude Code を使った開発
連休前の状態
- ✅ Stripe 連携の動作確認 (テストモード) は完了
- 🔴 LP は雛型のみ (フッターも
#プレースホルダ) - 🔴 法的ページ未作成
- 🔴 Stripe 本番審査未申請
- 🔴 独自ドメインなし
- 🔴 Clerk は Development Instance のまま
連休でやりたかったこと
「ユーザーが実際に決済できる状態」まで持っていく。連休前は「動く LP + テスト決済」までで、本番運用には何もできていなかった。
Day 1 (5/3): LP 完全実装
やったこと
- 価格戦略決定 (¥2,980/月、税抜)
- LP 8セクション実装 (ヒーロー、課題、機能、料金、FAQ など)
- フッターのリンクは
#プレースホルダのまま (法的ページがまだ無い)
判断ログ: なぜ無料トライアルを 7日間にしたか
価格と無料トライアル期間は、本番審査と密接に関わる重要な決定。判断軸は3つあった。
| 観点 | 検討した選択肢 | 採用 |
|---|---|---|
| 期間 | 3日 / 7日 / 14日 / 30日 | 7日 |
| カード登録タイミング | サインアップ時 / トライアル後 | サインアップ時 |
| 解約難易度 | 自社 UI / Stripe Customer Portal | Stripe Customer Portal |
7日間にした理由:
- 3日は短すぎる: ユーザーが価値を実感する前に終わる
- 14-30日は長すぎる: トライアルが惰性化する
- 7日が業界標準: 他のサブスク英語学習サービスと並ぶ
-
Stripe の
trial_period_daysで1行設定: 実装コスト最小
カード登録をサインアップ時にする選択は、離脱率は上がるがコンバージョン率も上がるトレードオフ。本番運用前なので、コンバージョン率を取る判断にした。
Day 2 (5/4 みどりの日): Stripe trial period 実装と詰まりポイント
Stripe trial period の実装は1行で終わる
LP に「7日間無料トライアル」と書いた以上、実装が必要。既存の Stripe Checkout コードに以下の1行を追加:
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
success_url: successUrl,
cancel_url: cancelUrl,
client_reference_id: userId,
subscription_data: {
trial_period_days: 7, // ← この1行を追加
metadata: { clerk_user_id: userId },
},
});
これで、ユーザーが checkout 完了しても8日目から課金が始まる。webhook 側の修正は不要 (既存の checkout.session.completed ハンドラがそのまま使える)。
コード修正は5分で済んだ。ここまでは順調だった。
詰まりポイント1: Clerk のレート制限
実装をデプロイ後、ブラウザでログインを試したら以下のエラーが出た。
Error: ClerkJS: Response: needs_client_trust not supported yet.
最初は「Clerk SDK のバージョンが古いのか?」と疑った。しかし @clerk/nextjs は ^6.37.4 で最新近い。
原因はレート制限だった。連続したログイン失敗で、Clerk のレート制限ロックアウトに引っかかっていた。
時間を置いて別ブラウザの新しいシークレットウィンドウで試したら、メール OTP 認証で普通にログインできた。エラーメッセージが「needs_client_trust」という分かりにくい表現だったので、レート制限とは思わなかった。
判断: SDK バージョン問題ではなくレート制限だった、と切り分けるのに 1時間かかった。
詰まりポイント2: Stripe Checkout 後のリダイレクトループ
ログインできるようになって、改めて Stripe Checkout を試したら、決済完了後に以下のループに陥った:
1. Stripe Checkout 完了
→ /practice?success=1 にリダイレクト
2. /practice は middleware で認証必須
→ Clerk セッション cookie が見つからず /sign-in にリダイレクト
3. /sign-in でログイン状態確認
→ セッションは intent-pika-39.accounts.dev ドメインに存在
4. 戻った先の biz-english-master.vercel.app から見えない
5. ループ
これは Clerk Development の構造的な問題だった:
- Clerk Development は
xxx.accounts.devという別ドメインで認証 - アプリのドメイン (
biz-english-master.vercel.app) と Cookie ドメインが分離 - Stripe Checkout 経由で外部に出て戻ってくると、Cookie が読めずに認証失敗
判断ログ: 中継ページで回避するか、Production に移行するか
| 選択肢 | 所要時間 | リスク | 判断 |
|---|---|---|---|
| /payment-success 中継ページ作成 | 30分 | 回避策、本質的解決にならない | ✅ 採用 |
| Clerk Production 移行 | 2-3時間 | 独自ドメイン取得が必要 | ⏸ 後回し |
Clerk Production 移行は本質的解決だが、その日中に動く状態にしたかったので、まず中継ページで回避することを選んだ。
実装した /payment-success ページは、認証不要の公開ページにして、5秒後に /practice に自動遷移する。Cookie 復元の時間を稼ぐ仕組み:
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
export default function PaymentSuccessPage() {
const [secondsLeft, setSecondsLeft] = useState(5);
const router = useRouter();
useEffect(() => {
const timer = setInterval(() => {
setSecondsLeft((prev) => {
if (prev <= 1) {
router.push("/practice");
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [router]);
return (
<main>
<h1>ご登録ありがとうございます</h1>
<p>7日間の無料トライアルが開始されました。</p>
<p>{secondsLeft}秒後に自動で進みます...</p>
</main>
);
}
middleware に "/payment-success" を public route として追加し、Stripe checkout の success_url を変更:
// 修正前
const successUrl = `${baseUrl}/practice?success=1`;
// 修正後
const successUrl = `${baseUrl}/payment-success`;
これで動作確認が完走した。「動く状態」を最優先にする判断だった。
法的ページ3種を作成
Stripe 本番審査では、日本のサブスク事業者として以下が必須:
- 特定商取引法に基づく表記
- プライバシーポリシー
- 利用規約
これを /legal、/privacy、/terms の3ページとして実装。プレースホルダ ([氏名]、[住所] 等) を残した状態で実装し、後で VS Code の Find & Replace で一括置換。
実装は Server Component で、外部依存なし。max-w-3xl で読みやすい横幅を確保し、Tailwind で整形。
LP のフッターのリンクも # から実 URL に置き換えた。これで Stripe 審査の前提条件が揃った。
Stripe 本番審査申請
Day 2 の最後に Stripe 本番審査を申請。
入力項目で印象的だったのは、日本のクレジット取引セキュリティ対策協議会に基づくセキュリティ申告書:
- 管理者画面のアクセス制限
- データディレクトリ露出対策
- Web アプリケーションの脆弱性対策
- マルウェア対策
- 不正ログイン対策
これらを「Yes/No」で答える必要がある。個人事業主として、Vercel + Clerk + Stripe を使っていれば実質的に対応できているものが多い (例: 不正ログイン対策の「ログイン試行回数制限」は Clerk が自動でやってくれている)。
判断: 「定期的な脆弱性診断を実施していますか?」には『Yes』と答えた。npm audit や Dependabot による依存関係の脆弱性チェックを継続的に運用していれば、これも広義の「定期的な脆弱性対策」に該当すると解釈した。
審査申請後、Stripe からは「本番環境を即時利用可能、審査結果は1-3営業日」のメッセージ。
Day 3 (5/5 こどもの日): Clerk Production 移行
vercel.app では Clerk Production が使えない
Day 2 に積み残した本質的問題、Clerk Production 移行に着手。
Clerk Production Instance を作成しようとしたら、以下のエラーで弾かれた:
*.vercel.app domains are not supported for production instances.
Please purchase a domain then try again.
Clerk Production には独自ドメインが必須。Vercel のホスティングドメインでは動かない。これは事前に調べていなかったので、ここで初めて知った。
判断ログ: 独自ドメインを今取るか、後回しにするか
| 選択肢 | コスト | 時間 | 判断 |
|---|---|---|---|
| 今すぐ独自ドメイン取得 | 年 ¥1,500-3,000 | 1-2時間 | ✅ 採用 |
| 連休後に取る | 同上 | 別日 | ❌ 却下 |
| Clerk から別の認証基盤に移行 | 0 | 数日 | ❌ 却下 |
今取る判断にした理由: 連休のスタミナがあるうちに本番運用準備を完了させたかった。連休後にやると「今日はやる気が出ない」が続いて伸びていく。
ドメイン取得とDNS設定
biz-english-master.com を、お名前.com で取得。LP のサービス名・リポジトリ名と完全一致させた。Stripe 明細書表記の BIZ ENGLISH MASTER とも統一できる。
Vercel でドメインを追加すると、必要な DNS レコードが指示される:
A @ 216.198.79.1
CNAME www 5a8a4c784eba2422.vercel-dns-017.com.
これをお名前.com の DNS 設定画面で追加。同時にネームサーバーを 01-04.dnsv.jp に変更。
ベストケースで 5-30分で反映、最大 72時間。今回は 10分で反映され、https://biz-english-master.com で LP が表示できた。
Clerk Production の DNS 設定
Clerk Production Instance を作成すると、5つの CNAME レコードを追加するよう指示される:
CNAME clerk → frontend-api.clerk.services
CNAME accounts → accounts.clerk.services
CNAME clkmail → mail.bspmijhmffol.clerk.services
CNAME clk._domainkey → dkim1.bspmijhmffol.clerk.services
CNAME clk2._domainkey → dkim2.bspmijhmffol.clerk.services
これらをお名前.com に追加。15分後、Clerk の Dashboard で 0/5 Verified から 5/5 Verified に変わった。
Google OAuth Production 用クライアント作成
Clerk Production では、Google ログイン用に Production 専用の OAuth クライアント が必要 (Development からはコピーされない)。
判断ログ:
| 選択肢 | 分離度 | 時間 | 判断 |
|---|---|---|---|
| 既存 Dev 用クライアントをそのまま使う | ❌ 混在 | 5分 | ❌ 却下 |
| 既存にリダイレクト URI を追加 | △ 一部分離 | 5分 | ❌ 却下 |
| Production 専用クライアントを新規作成 | ✅ 完全分離 | 15-20分 | ✅ 採用 |
完全分離を選んだ理由: 本番のセキュリティ事故が Development に波及しない、ユーザー分析が綺麗、面接で「Production と Development の OAuth を分離している」と説明できる。
Google Cloud Console で新規 OAuth クライアントを作成し、Clerk Production の Custom credentials に登録。
Vercel 環境変数の切り替えと動作確認
最後に、Vercel の環境変数を Production キーに切り替え:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: pk_test_xxx → pk_live_xxx
CLERK_SECRET_KEY: sk_test_xxx → sk_live_xxx
再デプロイ後、すべてのテストをパス:
- ✅
https://biz-english-master.comで LP 表示 - ✅ メール OTP ログイン成功
- ✅ Google ログイン成功
- ✅ Stripe Checkout 完走
- ✅ リダイレクトループ無し (Clerk Production の効果)
Day 4 (5/6 振替休日): Stripe 本番審査通過と本番運用開始
申請から24時間で審査通過
連休最終日の朝、Stripe からメールが届いた。
biz-english-master について、Stripe の準備が整ったことをお知らせします。
申請から約 24時間で審査通過。連休 4日間で「個人開発の B2C SaaS」が「実際に決済を受け付けられる状態」になった。
本番運用への切り替え作業
審査通過後、本番運用への切り替え作業がある:
-
Stripe Dashboard でビジネスサイト URL を更新:
vercel.app→.com(申請時は独自ドメイン未取得だった) -
本番モードの Webhook エンドポイント作成:
https://biz-english-master.com/api/stripe/webhook -
本番 API キー取得:
pk_live_xxx、sk_live_xxx -
本番商品の Price ID 確認: テストモードとは別の
price_xxx -
Vercel 環境変数を 5つ更新: Stripe Production キー × 4 +
NEXT_PUBLIC_APP_URLを.comに - 再デプロイ
本物のクレジットカードで動作確認
⚠️ ここで重要なのが、本番モードでは実際のクレジットカードを使うこと。ただし7日間無料トライアルなので ¥0 課金で動作確認できる。
1. https://biz-english-master.com で新規アカウント作成
2. /practice → アップグレードボタン
3. Stripe Checkout (本番モード) で実物のクレカ入力
4. 「申し込む」 → ¥0 課金 (trial 中)
5. /payment-success → /practice に遷移
6. Stripe Dashboard で trialing 状態確認
7. テスト完了後、即座にサブスクリプション解約 (8日後の自動課金を防ぐ)
trialing 中の即時解約は料金発生しない。これは Stripe 本番運用の動作確認に使える便利な仕組み。
連休 4日間で達成できたこと
✅ Stripe 本番審査通過 (申請から 24時間)
✅ 独自ドメイン取得・運用 (biz-english-master.com)
✅ Clerk Production 移行 (Cookie 問題解消)
✅ Google OAuth Production クライアント (Dev/Prod 分離)
✅ 本番モードでの実決済確認
✅ 法的ページ3種公開
LP 公開だけでは Stripe 本番審査は通らない。しかし、LP + 法的ページ + 中継ページの工夫で、個人開発でも十分通せることが分かった。
判断ログ: 何を採用、何を却下、何を後回しにしたか
連休 4日間で出した判断を一覧化する。
採用した判断
| 内容 | 理由 |
|---|---|
| Stripe trial period 7日に設定 | 業界標準、trial_period_days: 7 で1行 |
| /payment-success 中継ページ | Cookie 復元の時間稼ぎ、本質的解決の前段 |
| 独自ドメイン取得 (今すぐ) | Clerk Production への前提条件、連休のスタミナ活用 |
| Production 専用 Google OAuth クライアント | Dev/Prod 分離、セキュリティ向上 |
| 法的ページのプレースホルダ方式 | 個人情報を Chat に書かない運用 |
| 本番 Webhook を新規作成 | テスト用と本番用を完全分離 |
却下した判断
| 内容 | 理由 |
|---|---|
| Clerk から別の認証基盤への移行 | 移行コストが高すぎる、Clerk は良いツール |
| Google OAuth クライアントの共有 | Dev/Prod 混在のリスク |
| ドメイン取得を後回し | 連休最終日のスタミナを使い切る判断 |
後回しにした判断
| 内容 | 理由 |
|---|---|
| /payment-success 中継ページの削除 | Production 移行で不要だが、即削除はリスク |
| Vercel Sensitive 環境変数化 | 動作には影響しない警告レベル |
| middleware.ts → proxy.ts 移行 | Next.js 16 の deprecation 対応、急がない |
「却下」と「後回し」を分けるのは、Zenn 記事と同じ運用。やらないこと、今やらないことを明確にしておくと、後で判断軸を引き継げる。
詰まったポイント
1. Clerk の "needs_client_trust not supported yet" エラー
エラーメッセージから「SDK バージョン問題」と誤認した。実際の原因は 連続したログイン失敗によるレート制限だった。
教訓: エラーメッセージから即座に原因を判断しない。時間を置いて別ブラウザで試すと、別の症状になることがある。
2. vercel.app では Clerk Production が動かない
Clerk Production には独自ドメインが必須、という事前情報を持っていなかった。
教訓: 本番運用に必要な前提条件は、開発初期に確認しておく。連休前に分かっていれば、ドメイン取得を前倒しできた。
3. クレジット取引セキュリティ対策のチェックリスト
「定期的な脆弱性診断を実施していますか?」のような質問に「Yes」「No」で答える形式。書類を見た瞬間は「専門会社じゃないと回答できない」と感じたが、自分の運用 (npm audit、Dependabot 等) を整理して読み解くと、個人事業主でも該当する対応はしていることが分かった。
教訓: 「Yes」「No」の二択でも、自分の運用に照らして誠実に解釈する。チェックリストの目的は、事業者がセキュリティ意識を持っているかを確認すること。
4. Stripe 申請時の URL と運用時の URL が違う
審査時は vercel.app で申請したが、Day 3 で独自ドメインを取得した。Stripe アカウントの「ビジネスサイトURL」が古いままだと、後で混乱する可能性がある。
教訓: 本番運用切り替え時に、Stripe アカウントのビジネス情報も同期する。
まとめ: コード以外の戦いを楽しむ
LP 書ける、決済実装できる、認証連携できる。これらは個人開発のスタートラインだった。
連休 4日間で気付いたのは、コード以外の戦いが独立した筋肉として必要なことだった。具体的には:
- 法令対応の筋肉: 特商法、プライバシーポリシー、利用規約をテンプレートから自分の事業に合わせて書ける
- 審査対応の筋肉: Stripe や Apple の審査の「書類記入の作法」を理解できる
- 運用準備の筋肉: ドメイン取得、DNS 設定、本番/開発環境の分離ができる
- 判断の筋肉: 「採用」「却下」「後回し」を即決できる
これらは AI に丸投げしにくい領域だ。Claude Code に「特商法の表記を書いて」と言ってもテンプレートは出てくるが、自分の事業に合わせる判断は人間がやる必要がある。
個人開発で本番運用までいくのは、想像していたより遠い道のりだった。同時に、想像していたほど不可能でもなかった。1個ずつ判断して、判断ログを残す。これが連休 4日間でも、その先でも変わらない原則だと思う。
申請から 24時間で本番審査が通った。これは「個人開発でもまっとうな手順を踏めば、Stripe は通してくれる」という実証だ。
おまけ: 使ったコードと設定の総量
参考までに、4日間で書いた / 設定したものの量。
コード
-
/legal/page.tsx— 165行 (特商法表記) -
/privacy/page.tsx— 218行 (プライバシーポリシー) -
/terms/page.tsx— 213行 (利用規約) -
/payment-success/page.tsx— 53行 (中継ページ) -
middleware.ts— 4行追加 (public route 4つ) -
api/stripe/checkout/route.ts— 1行追加 (trial_period_days: 7)、1行変更 (success_url)
設定
- お名前.com — DNS レコード 11個 (Vercel 2 + Clerk 5 + NS 4)
- Vercel — 環境変数 7つ更新 (Clerk × 2 + Stripe × 4 + NEXT_PUBLIC_APP_URL)
- Clerk Production Instance — 1つ作成、Google OAuth Custom credentials 登録
- Google Cloud — Production 用 OAuth クライアント1つ作成
- Stripe — 本番審査申請 → 通過、本番 Webhook 作成、ビジネス URL 更新
コード量は驚くほど少ない。ほとんどの時間が判断と設定だった。
ケーススタディリポジトリ: (近日公開予定)
Zenn 記事: Claude Code が出した提案を却下する技術