AI SaaS MVP 開発手順書

Page content

まず、フロントエンドデザインのスキルを確認します。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 ファイルを作成:

# Clerk
PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...

# Supabase
PUBLIC_SUPABASE_URL=https://xxx.supabase.co
PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...

# Stripe
PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Anthropic
ANTHROPIC_API_KEY=sk-ant-...

Cloudflare Pages の場合は、ダッシュボードの「Settings > Environment variables」にも同じ値を登録する。


Phase 2 — 認証(Clerk)(1時間)

2-1. ミドルウェア設定

src/middleware.ts を作成:

import { clerkMiddleware, createRouteMatcher } from '@clerk/astro/server';

const isProtected = createRouteMatcher(['/dashboard(.*)']);

export const onRequest = clerkMiddleware((auth, context) => {
  if (isProtected(context.request) && !auth().userId) {
    return auth().redirectToSignIn();
  }
});

2-2. サインイン/アップページ

src/pages/sign-in.astro:

---
import { SignIn } from '@clerk/astro/components';
---
<SignIn />

src/pages/sign-up.astro:

---
import { SignUp } from '@clerk/astro/components';
---
<SignUp />

2-3. レイアウトにユーザー情報を表示

---
import { UserButton, SignedIn, SignedOut } from '@clerk/astro/components';
---
<SignedIn>
  <UserButton />
</SignedIn>
<SignedOut>
  <a href="/sign-in">ログイン</a>
</SignedOut>

Phase 3 — データベース(Supabase)(1〜2時間)

3-1. テーブル設計(MVP最小構成)

Supabase SQL Editorで実行:

-- ユーザープロフィール(Clerkのuser_idと紐付け)
create table profiles (
  id          uuid primary key default gen_random_uuid(),
  clerk_id    text unique not null,
  email       text,
  plan        text default 'free',   -- 'free' | 'pro'
  created_at  timestamptz default now()
);

-- AI処理結果(メインの価値提供テーブル)
create table leads (
  id          uuid primary key default gen_random_uuid(),
  user_id     uuid references profiles(id) on delete cascade,
  input       text not null,
  result      jsonb,
  created_at  timestamptz default now()
);

-- Row Level Security
alter table profiles enable row level security;
alter table leads    enable row level security;

create policy "own profile" on profiles
  for all using (clerk_id = current_setting('app.clerk_id', true));

create policy "own leads" on leads
  for all using (
    user_id = (
      select id from profiles
      where clerk_id = current_setting('app.clerk_id', true)
    )
  );

3-2. Supabaseクライアント

src/lib/supabase.ts:

import { createClient } from '@supabase/supabase-js';

// ブラウザ用(anon key)
export const supabase = createClient(
  import.meta.env.PUBLIC_SUPABASE_URL,
  import.meta.env.PUBLIC_SUPABASE_ANON_KEY,
);

// サーバー用(service role key)
export const supabaseAdmin = createClient(
  import.meta.env.PUBLIC_SUPABASE_URL,
  import.meta.env.SUPABASE_SERVICE_ROLE_KEY,
);

// Clerk ID を RLS に渡すヘルパー
export function supabaseWithUser(clerkId: string) {
  return createClient(
    import.meta.env.PUBLIC_SUPABASE_URL,
    import.meta.env.PUBLIC_SUPABASE_ANON_KEY,
    {
      global: {
        headers: { 'x-clerk-id': clerkId },
      },
    }
  );
}

Phase 4 — AI機能(Claude Sonnet)(1〜2時間)

4-1. API Routeの作成

src/pages/api/ai/generate.ts:

import type { APIRoute } from 'astro';
import Anthropic from '@anthropic-ai/sdk';
import { getAuth } from '@clerk/astro/server';
import { supabaseAdmin } from '../../../lib/supabase';

export const POST: APIRoute = async ({ request, locals }) => {
  const { userId } = getAuth(locals);
  if (!userId) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
  }

  const { input } = await request.json();

  // プラン確認(pro以外は制限など)
  const { data: profile } = await supabaseAdmin
    .from('profiles')
    .select('id, plan')
    .eq('clerk_id', userId)
    .single();

  if (!profile) {
    return new Response(JSON.stringify({ error: 'Profile not found' }), { status: 404 });
  }

  // Claude API呼び出し
  const client = new Anthropic({ apiKey: import.meta.env.ANTHROPIC_API_KEY });

  const message = await client.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 1024,
    messages: [
      {
        role: 'user',
        content: `以下の情報から新規顧客獲得のためのアプローチを提案してください:\n\n${input}`,
      },
    ],
  });

  const result = message.content[0].type === 'text' ? message.content[0].text : '';

  // Supabaseに保存
  await supabaseAdmin.from('leads').insert({
    user_id: profile.id,
    input,
    result: { text: result },
  });

  return new Response(JSON.stringify({ result }), { status: 200 });
};

4-2. フロントエンドからの呼び出し例

async function generateLead(input: string) {
  const res = await fetch('/api/ai/generate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ input }),
  });
  const { result } = await res.json();
  return result;
}

Phase 5 — 課金(Stripe)(2〜3時間)

5-1. Checkout Sessionの作成

src/pages/api/stripe/checkout.ts:

import type { APIRoute } from 'astro';
import Stripe from 'stripe';
import { getAuth } from '@clerk/astro/server';

const stripe = new Stripe(import.meta.env.STRIPE_SECRET_KEY);

export const POST: APIRoute = async ({ request, locals, url }) => {
  const { userId } = getAuth(locals);
  if (!userId) return new Response('Unauthorized', { status: 401 });

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [
      {
        price: 'price_XXXXX', // Stripeダッシュボードで作成した価格ID
        quantity: 1,
      },
    ],
    metadata: { clerk_id: userId },
    success_url: `${url.origin}/dashboard?upgraded=true`,
    cancel_url:  `${url.origin}/pricing`,
  });

  return Response.redirect(session.url!, 303);
};

5-2. Webhookハンドラー

src/pages/api/stripe/webhook.ts:

import type { APIRoute } from 'astro';
import Stripe from 'stripe';
import { supabaseAdmin } from '../../../lib/supabase';

const stripe = new Stripe(import.meta.env.STRIPE_SECRET_KEY);

export const POST: APIRoute = async ({ request }) => {
  const sig = request.headers.get('stripe-signature')!;
  const body = await request.text();

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body, sig, import.meta.env.STRIPE_WEBHOOK_SECRET
    );
  } catch {
    return new Response('Webhook signature failed', { status: 400 });
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object as Stripe.CheckoutSession;
    const clerkId = session.metadata?.clerk_id;

    if (clerkId) {
      await supabaseAdmin
        .from('profiles')
        .update({ plan: 'pro' })
        .eq('clerk_id', clerkId);
    }
  }

  if (event.type === 'customer.subscription.deleted') {
    // サブスク解約時にfreeへ降格
    // session.customer でStripe顧客IDからclerk_idを引くロジックを追加
  }

  return new Response('OK');
};

5-3. StripeのWebhook登録

# ローカル開発時
stripe listen --forward-to localhost:4321/api/stripe/webhook

# 本番はStripeダッシュボードからhttps://your-domain/api/stripe/webhook を登録

Phase 6 — デプロイ(Cloudflare)(1時間)

6-1. ビルド設定

package.json に追加:

{
  "scripts": {
    "build": "astro build",
    "preview": "astro preview"
  }
}

6-2. Cloudflare Pages へデプロイ

# Wrangler CLI インストール
npm install -g wrangler

# ログイン
wrangler login

# デプロイ
wrangler pages deploy dist/

または GitHub連携で自動デプロイ(推奨):

Cloudflareダッシュボード → Workers & Pages → Create → Connect to Git → リポジトリ選択 → Build command: npm run build / Output directory: dist

6-3. 環境変数の登録

Cloudflareダッシュボード → Pages → Settings → Environment variables に .env の全変数を登録する。


Phase 7 — メール(Resend)※後回しOK

後からResendを追加する場合の手順。

npm install resend

src/lib/email.ts:

import { Resend } from 'resend';

const resend = new Resend(import.meta.env.RESEND_API_KEY);

export async function sendWelcomeEmail(to: string, name: string) {
  await resend.emails.send({
    from: 'noreply@yourdomain.com',
    to,
    subject: 'ご登録ありがとうございます',
    html: `<p>${name}様、ご登録ありがとうございます。</p>`,
  });
}

Webhook (checkout.session.completed) 内でプラン更新後に呼び出す。


開発順序まとめ

順序 フェーズ 目安時間 完了条件
1 セットアップ 1h npm run dev が起動する
2 Clerk認証 1h サインイン・保護ルートが動く
3 Supabase DB 1〜2h プロフィール作成・RLSが効く
4 Claude AI 1〜2h API経由でAI結果が返る
5 Stripe課金 2〜3h 決済→プランアップが完了する
6 Cloudflareデプロイ 1h 本番URLで動く
7 Resendメール 1h ウェルカムメールが届く

合計目安: 7〜10時間 でMVP稼働。Stripeのwebhookテストが最も詰まりやすいポイントなので、stripe listen のローカルテストを先に十分やっておくのがおすすめです。