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 ファイルを作成:
# 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 のローカルテストを先に十分やっておくのがおすすめです。