徒然

AI SaaS MVP 開発手順書

まず、フロントエンドデザインのスキルを確認します。MVP開発手順書を作成します。—

AI SaaS MVP 開発手順書

スタック: Astro + Clerk + Supabase + Stripe + Claude Sonnet + Cloudflare


Phase 0 — 事前準備(30分)

各サービスのアカウントを作成してAPIキーを取得する。

  • Clerk: clerk.com でアプリ作成 → Publishable Key / Secret Key
  • Supabase: supabase.com でプロジェクト作成 → URL / anon key / service_role key
  • Stripe: stripe.com でアカウント作成 → Publishable Key / Secret Key / Webhook Secret
  • Anthropic: console.anthropic.com → API Key
  • Cloudflare: cloudflare.com でアカウント作成(Pages + Workers 無料枠で可)
  • Resend(後回しOK): resend.com → API Key

Phase 1 — プロジェクトセットアップ(1時間)

1-1. Astroプロジェクト作成

npm create astro@latest my-saas -- --template minimal --typescript strict
cd my-saas
npm install

1-2. 必要パッケージインストール

# Clerk (Astro統合)
npm install @clerk/astro

# Supabase
npm install @supabase/supabase-js

# Stripe
npm install stripe @stripe/stripe-js

# AI SDK (Anthropic)
npm install @anthropic-ai/sdk

# Cloudflare Adapter
npm install @astrojs/cloudflare

1-3. astro.config.mjs 設定

import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
import clerk from '@clerk/astro';

export default defineConfig({
  output: 'server',
  adapter: cloudflare(),
  integrations: [clerk()],
});

1-4. 環境変数設定

.env ファイルを作成:

Supabase 無料枠

Supabase 無料枠でDBが停止・制限される条件は大きく2つあります。


① 非アクティブによる自動一時停止(Pause)

無料プランでは、7日間(1週間)アクティビティがないとプロジェクトが自動的に一時停止されます。停止前にSupabaseから警告メールが届きます。

「アクティビティ」として認識されるのは、REST APIへのリクエストやDBへの直接クエリです。静的なホームページへのアクセスだけではDBへのアクセスと見なされないため、停止を防げないケースがあります。

再開方法: ダッシュボードから手動でUnpauseするだけで復旧できます。

停止を防ぐ方法:

  • GitHub Actions などで週2回程度、DBへの軽量クエリを定期実行する
  • Supabase Edge Functions の pg_cron + pg_net で自前のkeep-aliveを設定する

一時停止されたプロジェクトは90日間はダッシュボードから復元可能です。それ以降はデータが削除される点に注意が必要です。


② 使用量超過による機能制限(Fair Use Policy)

無料プランの上限を超えると、請求先メールに通知が届き、**グレースピリオド(猶予期間)**が与えられます。猶予期間が終了しても使用量を減らさないか、Proへアップグレードしない場合、サービス制限が適用されます。

主な上限の目安(2026年現在):

リソース 無料枠上限
DBサイズ 500 MB
ファイルストレージ 1 GB
DBエグレス 5 GB / 月
月間アクティブユーザー 50,000 MAU

スタートアップへの実務的な注意点

有料プラン(Pro以上)ではプロジェクトが一時停止されることはありません。実際にユーザーがいるプロダクションアプリを無料枠で動かす場合、実トラフィックがあれば自動停止は発生しませんが、開発・ステージング環境は1週間放置すると止まります。MVP検証フェーズを超えたタイミングで Pro($25/月)への移行を検討するのが現実的です。

返信が来る状態を作るSaaS

いい構成です。ただしそのまま作ると**「ただのツール」で終わるリスクが高い**ので、最初にMVPの“勝ち筋”を固定します。

結論:
👉 「営業リスト生成」ではなく「返信が来る状態を作る」SaaSにする


① MVPのコア定義(ここがズレると全部死ぬ)

❌ よくある失敗

  • リード抽出

  • メール生成

  • CRMっぽい管理

👉 競合(Apollo / Outreach / Salesloft)に勝てない


✅ MVPの正解

👉 「1クリックで“返信される営業”を作る」

最小価値:

  • ターゲット入力

  • AIが「刺さる理由」を生成

  • DM/メール文生成

  • 送信 or コピー


② MVP機能(削りまくり版)

必須機能(これだけ)

  1. ユーザー登録・ログイン(Clerk)

  2. ターゲット入力フォーム

  3. AI生成(Sonnet)

  4. 結果表示(営業文 + 理由)

  5. 保存(Supabase)

  6. 課金(Stripe:後からでOK)


削る機能(絶対やらない)

  • CRM

  • チーム機能

  • 自動送信(最初は不要)

  • 分析ダッシュボード

👉 “生成して終わり”でいい


③ 全体アーキテクチャ

[Astro Frontend]
   ↓
[Cloudflare Workers API]
   ↓
[Clerk Auth]
   ↓
[Supabase DB]
   ↓
[Claude Sonnet API]

④ DB設計(Supabase)

users(Clerk連携)

id (Clerk user_id)
email
created_at

generations

id
user_id
target_input
ai_output
created_at

⑤ 画面設計(Astro)

① LP

  • 「返信が来る営業をAIで作る」

AIを活用したSaaS

この3つ、表面的には似ていますが、思想がまったく違うプロダクトです。
単なる機能比較ではなく、「どの戦い方をするか」で選ぶものです。


■ まず結論(超重要)

  • Apollo
     👉 オールインワン(データ+実行)で高速立ち上げ

  • Outreach
     👉 大企業向けの高度な営業オペレーション

  • Salesloft
     👉 現場が使いやすいバランス型


■ 一発で分かる比較表

項目 Apollo Outreach Salesloft
コンセプト オールインワンGTM 営業実行プラットフォーム エンゲージメント特化
リードデータ ◎ 内蔵(数億件) ✕ 外部依存 ✕ 外部依存
アウトリーチ
AI機能 ◎(軽量・実用) ◎(高度・予測) ○(改善系)
CRM連携 ◎(深い)
使いやすさ △(重い)
価格 ◎(安い) ✕(高い) △(中〜高)
向いてる企業 スタートアップ〜中小 エンタープライズ 中堅〜大企業

■ ① Apollo(最も“今っぽい”)

特徴

  • リードデータ + アウトリーチが一体化

  • 数億件の連絡先DBを内蔵 (Apollo)

BDRとSDRとABM

インサイドセールス(内勤営業)における BDRSDR の主な違いは、簡単に言うと**「攻めの営業」「受けの営業」**かという点にあります。

両者はどちらも「商談を創出してフィールドセールスに繋ぐ」という役割は共通していますが、ターゲットとする顧客層やアプローチ手法が大きく異なります。


BDRとSDRの比較表

項目 BDR (Business Development Representative) SDR (Sales Development Representative)
主な手法 アウトバウンド(攻め) インバウンド(受け)
ターゲット 新規開拓、エンタープライズ(大手企業) 問い合わせ、資料請求があった見込み客
アプローチ先 接点のないターゲット企業へ戦略的に接触 自社に興味を持ってくれた層へ迅速に対応
難易度 高い(接点ゼロから関心を持たせる) 比較的低い(すでに関心がある)
主なKPI 有効商談数、ターゲット企業への接触率 商談化率、リード対応スピード

1. BDR (Business Development Representative)

BDRは、自社が狙いたい**特定の企業(ターゲットアカウント)**に対して、こちらから能動的に仕掛ける役割です。

  • 特徴: 手紙、電話、SNS(LinkedInなど)を駆使し、決裁権者(役員クラスなど)に直接アプローチすることが多いです。

  • 強み: 自社にとって理想的な「大口顧客」を戦略的に狙い撃ちできるため、成約時のインパクトが非常に大きくなります(ABM:アカウント・ベースド・マーケティングの考え方に近いです)。

2. SDR (Sales Development Representative)

SDRは、ウェブサイトからの資料請求や展示会などで獲得した**「リード(見込み客)」**に対応する役割です。

  • 特徴: 「興味がある」と手を挙げた人に対して、熱が冷めないうちに素早く連絡を取り、課題をヒアリングして商談を設定します。

  • 強み: すでにニーズが顕在化している層を扱うため、効率的に多くの商談を生み出すことができます。


なぜ使い分けるのか?

多くのB2B企業では、**「効率よく数を稼ぐSDR」「時間はかかるが大きな利益を狙うBDR」**を組み合わせることで、安定した売上成長を目指します。

例えば、中小企業からの問い合わせはSDRが効率的に裁き、一方で市場シェア拡大に不可欠な大手企業に対してはBDRがじっくりと戦略的な網を張る、といった使い分けが一般的です。

ABM(アカウント・ベースド・マーケティング)BDR/SDRの違いを一言で言えば、**「戦略(考え方)」「実行部隊(役割)」**かという違いです。

ABMは「どの企業を攻めるか」という組織全体の戦略を指し、BDRやSDRはその戦略を実現するための「人・チーム」を指します。


ABMとインサイドセールス(BDR/SDR)の関係図

ABMという大きな戦略の枠組みの中に、実行フェーズとしてBDR(またはSDR)が組み込まれているイメージです。

項目 ABM BDR / SDR
定義 戦略・コンセプト 役割・職種(実行担当)
視点 「点(個人)」ではなく**「面(企業単位)」** 「リード(個人)」へのアプローチ
範囲 マーケ・営業・CS・経営層の連携 主にインサイドセールス部門の活動
目的 ターゲット企業の売上最大化 商談の創出(アポイント獲得)

1. ABMは「全社的な作戦」

ABMは、不特定多数にアプローチするのではなく、**「利益への貢献度が高い特定の企業(アカウント)」**を定義し、その企業専用のラブレターを送るようなマーケティング手法です。

Stripeのテストモード

Stripeのテストモードは、「本番の決済を動かす前に、完全に安全な環境で課金フローを検証する」ための仕組みです。実際のお金は一切動きません。

ここでは、最短で動かす手順+実務でハマるポイントまでまとめます。


① テストモードの基本切り替え

まずはここが入口です。

  • Stripe Dashboard にログイン

  • 右上の 「テストモード」トグルをON

👉 これで「完全に別の世界」に切り替わります
(本番データとは一切混ざらない)


② APIキーをテスト用に変更(重要)

Stripeはキーが全てです。

  • テスト用キー(使う)

    • pk_test_xxx(公開キー)

    • sk_test_xxx(秘密キー)

  • 本番用キー(絶対混ぜない)

    • pk_live_xxx

    • sk_live_xxx

👉 よくあるミス
「フロントはtest、サーバーはlive」=事故確定


③ 商品・価格をテスト環境で作る

テストモード中に作る必要があります。

  • 商品(Product)作成

  • 価格(Price)設定(月額・年額など)

👉 注意
本番モードとは別なので
本番リリース時は“作り直し”になる


④ テストカードで決済する

Stripeは専用のテストカード番号を用意しています。

代表例:

  • 成功するカード
    4242 4242 4242 4242

  • 失敗するカード
    4000 0000 0000 9995

  • 3Dセキュア対応
    4000 0027 6000 3184

👉 入力情報(例)

  • 有効期限:未来の日付

  • CVC:適当(例:123)


⑤ Webhookをテストする(ここが本番差分)

決済後の処理はWebhookで動きます。

例:

  • checkout.session.completed

  • invoice.paid

ローカルでやる場合:

stripe listen --forward-to localhost:8787/webhook

👉 Stripe CLI を使うと便利

TypeScript

TypeScriptは、ざっくり言うと
👉 JavaScriptに「型(type)」を足して安全にした言語です。

「文法」を最短で使えるレベルに絞って解説します。


① 基本の型(ここが出発点)

let name: string = "Seiichi";
let age: number = 30;
let isActive: boolean = true;

👉 型を書くだけで

  • バグ減る

  • 補完効く


② 配列・オブジェクト

let numbers: number[] = [1, 2, 3];

let user: { id: string; age: number } = {
  id: "u1",
  age: 25,
};

③ 関数

function add(a: number, b: number): number {
  return a + b;
}

👉 : number は戻り値の型


④ 型エイリアス(実務で超使う)

type User = {
  id: string;
  email: string;
  isActive: boolean;
};

const user: User = {
  id: "1",
  email: "test@test.com",
  isActive: true,
};

👉 長い型は type でまとめる


⑤ Optional(任意プロパティ)

type User = {
  id: string;
  email?: string;
};

👉 ? があると無くてもOK


⑥ Union型(どっちか)

let status: "active" | "trialing" | "canceled";

👉 Stripe系ではこれ必須

エレクテイオン神殿

エレクテイオン(Erechtheion)は、アテネのアクロポリスに建つ古代ギリシャの神殿です。## 概要

紀元前421〜406年頃に建てられたイオニア式建築で、パルテノン神殿の北側に位置しています。

主な特徴

祀られた神々・英雄 アテナ女神、海神ポセイドン、伝説の王エレクテウスなど、複数の神・英雄を一つの神殿で祀っていました。

カリアティード(女人像柱) 最大の見どころは「乙女の回廊(ポーチ)」で、通常の柱の代わりに6体の女性像が屋根を支えています。現在、オリジナルの1体はロンドンの大英博物館に所蔵されており、残りはアテネのアクロポリス博物館で保管されています。

複雑な構造 建物は複数の標高差がある聖域をひとつにまとめた、非常に珍しい非対称な設計になっています。

歴史的背景

  • アクロポリスで最も神聖な場所のひとつとされていました。
  • 伝説では、ここでアテナとポセイドンがアテネの守護神の座を争ったとされています(アテナが勝利)。
  • 中世にはキリスト教の教会、オスマン帝国時代にはハーレムとして使用された歴史もあります。

パルテノン神殿に比べて小ぶりですが、カリアティードの優美さと複雑な建築構成が際立つ、アクロポリスを代表する建造物のひとつです。

危険人物チェックリスト


危険人物チェックリスト(越境営業・実戦版)

前提

このタイプ(知的・国際・交渉型)はリターンも大きいが、外すと損失も大きい。

判断基準はこれに固定:

言語ではなく「行動」と「一貫性」で判断する


チェックリスト(6タイプ)


① 知的だけで動かないタイプ

特徴
・抽象化・構造化は得意
・具体に落とさない
・決断を避ける

見抜き質問
“If you had to choose one structure, what would you do first?”

判定
・安全:1つに絞り、現実的な理由を説明
・危険:「It depends」で逃げ続ける


② ネットワークだけアピールするタイプ

特徴
・「知り合いが多い」と言う
・具体的な接続が出ない
・紹介を匂わせるだけ

見抜き質問
“Who would be the first person you’d realistically approach?”

判定
・安全:実名 or 明確な属性+接触方法
・危険:「後で繋げる」で曖昧


③ フリーライダー(成果乗っかり型)

特徴
・意見は出すが手は動かさない
・成果が出ると関与を主張

見抜き方
小さなタスクを渡す


「具体的なパートナー候補を1社出してほしい」

判定
・安全:自分で調べて具体を出す
・危険:議論に戻す・先延ばし


④ 過剰コミット型(後で消える)

特徴
・最初だけ異常に熱い
・すぐ「やろう」と言う
・実行フェーズで失速

見抜き質問
“What time can you realistically commit per week?”

Workers + Hono + D1 + Stripe

了解。**そのままコピペで動く最小構成(実用レベル)**を出します。
構成は👇

  • Cloudflare Workers

  • Hono

  • Cloudflare D1

  • Stripe


■ 完成構成

project/
 ├ wrangler.toml
 ├ package.json
 └ src/
     └ index.ts

■ ① package.json

{
  "name": "workers-hono-stripe",
  "type": "module",
  "dependencies": {
    "hono": "^4.0.0",
    "stripe": "^14.0.0"
  }
}

■ ② wrangler.toml

name = "hono-stripe-app"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[[d1_databases]]
binding = "DB"
database_name = "app-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

[vars]
STRIPE_SECRET_KEY = "sk_test_xxx"
STRIPE_WEBHOOK_SECRET = "whsec_xxx"

■ ③ D1 初期テーブル

CREATE TABLE users (
  id TEXT PRIMARY KEY,
  email TEXT,
  stripe_customer_id TEXT,
  created_at TEXT
);

CREATE TABLE subscriptions (
  id TEXT PRIMARY KEY,
  user_id TEXT,
  status TEXT,
  price_id TEXT,
  current_period_end INTEGER
);

■ ④ src/index.ts(コア)

import { Hono } from 'hono'
import Stripe from 'stripe'

type Bindings = {
  DB: D1Database
  STRIPE_SECRET_KEY: string
  STRIPE_WEBHOOK_SECRET: string
}

const app = new Hono<{ Bindings: Bindings }>()

/* -----------------------------
   Stripe 初期化
----------------------------- */
const getStripe = (env: Bindings) => {
  return new Stripe(env.STRIPE_SECRET_KEY, {
    apiVersion: '2023-10-16'
  })
}

/* -----------------------------
   ヘルスチェック
----------------------------- */
app.get('/', (c) => {
  return c.text('OK')
})

/* -----------------------------
   Checkout作成
----------------------------- */
app.post('/create-checkout', async (c) => {
  const stripe = getStripe(c.env)

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [
      {
        price: 'price_xxx', // ← StripeのPrice ID
        quantity: 1
      }
    ],
    success_url: 'https://example.com/success',
    cancel_url: 'https://example.com/cancel'
  })

  return c.json({ url: session.url })
})

/* -----------------------------
   Webhook(超重要)
----------------------------- */
app.post('/webhook', async (c) => {
  const stripe = getStripe(c.env)

  const sig = c.req.header('stripe-signature')
  const body = await c.req.text()

  let event

  try {
    event = stripe.webhooks.constructEvent(
      body,
      sig!,
      c.env.STRIPE_WEBHOOK_SECRET
    )
  } catch (err) {
    return c.text('Webhook Error', 400)
  }

  switch (event.type) {
    case 'checkout.session.completed':
      const session = event.data.object as any

      await c.env.DB.prepare(
        `INSERT INTO subscriptions (id, user_id, status)
         VALUES (?, ?, ?)`
      )
        .bind(session.id, session.customer, 'active')
        .run()
      break

    case 'customer.subscription.deleted':
      const sub = event.data.object as any

      await c.env.DB.prepare(
        `UPDATE subscriptions SET status = ? WHERE id = ?`
      )
        .bind('canceled', sub.id)
        .run()
      break
  }

  return c.text('ok')
})

/* -----------------------------
   課金チェック(SaaSガード)
----------------------------- */
app.get('/protected', async (c) => {
  const userId = 'test_user' // ← 本来は認証から取得

  const result = await c.env.DB.prepare(
    `SELECT * FROM subscriptions WHERE user_id = ? AND status = 'active'`
  )
    .bind(userId)
    .first()

  if (!result) {
    return c.text('Not subscribed', 403)
  }

  return c.text('Welcome premium user')
})

export default app

■ ⑤ デプロイ手順

npm install
npx wrangler d1 create app-db
npx wrangler publish

■ ⑥ Stripe側設定

やること👇