mcpField
S
saas-scaffolder

saas-scaffolder

github.com/alirezarezvani開発28.9k632 回インストール2 日前に更新
リポジトリ

saas-scaffolder について

プロダクトチーム向けに、SaaS アプリの土台作りを段階的に案内するスキルです。Next.js と TypeScript の構築に加え、認証、データベース、決済方式を選び、各段階で検証できます。

主な機能

  1. 01SaaS プロジェクト構成の生成
  2. 02Next.js・TypeScript・App Router の設定
  3. 03認証とデータベース方式の選択
  4. 04決済ルートと Webhook の構築
  5. 05ビルドと動作確認による段階検証

活用シーン

  1. 01製品説明から SaaS アプリを開始
  2. 02認証機能と保護されたダッシュボードの構築
  3. 03サブスクリプション決済と請求ページの追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/alirezarezvani/claude-skills --skill saas-scaffolder

インストール後、Claude Code で/saas-scaffolderを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
architecture-patterns.md
auth-billing-guide.md
saas-architecture-patterns.md
tech-stack-comparison.md
scripts/
project_bootstrapper.py

詳細

SaaS スキャフォルダー

Tier: 強力
Category: プロダクトチーム
Domain: フルスタック開発/プロジェクトの初期設定


入力形式

Product: [name]
Description: [1-3 sentences]
Auth: nextauth | clerk | supabase
Database: neondb | supabase | planetscale
Payments: stripe | lemonsqueezy | none
Features: [comma-separated list]

ファイルツリー出力

my-saas/
├── app/
│   ├── (auth)/
│   │   ├── login/page.tsx
│   │   ├── register/page.tsx
│   │   └── layout.tsx
│   ├── (dashboard)/
│   │   ├── dashboard/page.tsx
│   │   ├── settings/page.tsx
│   │   ├── billing/page.tsx
│   │   └── layout.tsx
│   ├── (marketing)/
│   │   ├── page.tsx
│   │   ├── pricing/page.tsx
│   │   └── layout.tsx
│   ├── api/
│   │   ├── auth/[...nextauth]/route.ts
│   │   ├── webhooks/stripe/route.ts
│   │   ├── billing/checkout/route.ts
│   │   └── billing/portal/route.ts
│   └── layout.tsx
├── components/
│   ├── ui/
│   ├── auth/
│   │   ├── login-form.tsx
│   │   └── register-form.tsx
│   ├── dashboard/
│   │   ├── sidebar.tsx
│   │   ├── header.tsx
│   │   └── stats-card.tsx
│   ├── marketing/
│   │   ├── hero.tsx
│   │   ├── features.tsx
│   │   ├── pricing.tsx
│   │   └── footer.tsx
│   └── billing/
│       ├── plan-card.tsx
│       └── usage-meter.tsx
├── lib/
│   ├── auth.ts
│   ├── db.ts
│   ├── stripe.ts
│   ├── validations.ts
│   └── utils.ts
├── db/
│   ├── schema.ts
│   └── migrations/
├── hooks/
│   ├── use-subscription.ts
│   └── use-user.ts
├── types/index.ts
├── middleware.ts
├── .env.example
├── drizzle.config.ts
└── next.config.ts

主要なコンポーネントパターン

認証設定(NextAuth)
// lib/auth.ts
import { NextAuthOptions } from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import { DrizzleAdapter } from "@auth/drizzle-adapter"
import { db } from "./db"

export const authOptions: NextAuthOptions = {
  adapter: DrizzleAdapter(db),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    session: async ({ session, user }) => ({
      ...session,
      user: {
        ...session.user,
        id: user.id,
        subscriptionStatus: user.subscriptionStatus,
      },
    }),
  },
  pages: { signIn: "/login" },
}
データベーススキーマ(Drizzle + NeonDB)
// db/schema.ts
import { pgTable, text, timestamp, integer } from "drizzle-orm/pg-core"

export const users = pgTable("users", {
  id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
  name: text("name"),
  email: text("email").notNull().unique(),
  emailVerified: timestamp("emailVerified"),
  image: text("image"),
  stripeCustomerId: text("stripe_customer_id").unique(),
  stripeSubscriptionId: text("stripe_subscription_id"),
  stripePriceId: text("stripe_price_id"),
  stripeCurrentPeriodEnd: timestamp("stripe_current_period_end"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
})

export const accounts = pgTable("accounts", {
  userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
  type: text("type").notNull(),
  provider: text("provider").notNull(),
  providerAccountId: text("provider_account_id").notNull(),
  refresh_token: text("refresh_token"),
  access_token: text("access_token"),
  expires_at: integer("expires_at"),
})
Stripe チェックアウトルート
// app/api/billing/checkout/route.ts
import { NextResponse } from "next/server"
import { getServerSession } from "next-auth"
import { authOptions } from "@/lib/auth"
import { stripe } from "@/lib/stripe"
import { db } from "@/lib/db"
import { users } from "@/db/schema"
import { eq } from "drizzle-orm"

export async function POST(req: Request) {
  const session = await getServerSession(authOptions)
  if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })

  const { priceId } = await req.json()
  const [user] = await db.select().from(users).where(eq(users.id, session.user.id))

  let customerId = user.stripeCustomerId
  if (!customerId) {
    const customer = await stripe.customers.create({ email: session.user.email! })
    customerId = customer.id
    await db.update(users).set({ stripeCustomerId: customerId }).where(eq(users.id, user.id))
  }

  const checkoutSession = await stripe.checkout.sessions.create({
    customer: customerId,
    mode: "subscription",
    payment_method_types: ["card"],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?upgraded=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
    subscription_data: { trial_period_days: 14 },
  })

  return NextResponse.json({ url: checkoutSession.url })
}
ミドルウェア
// middleware.ts
import { withAuth } from "next-auth/middleware"
import { NextResponse } from "next/server"

export default withAuth(
  function middleware(req) {
    const token = req.nextauth.token
    if (req.nextUrl.pathname.startsWith("/dashboard") && !token) {
      return NextResponse.redirect(new URL("/login", req.url))
    }
  },
  { callbacks: { authorized: ({ token }) => !!token } }
)

export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*", "/billing/:path*"],
}
環境変数テンプレート
# .env.example
NEXT_PUBLIC_APP_URL=http://localhost:3000
DATABASE_URL=postgresql://user:[email protected]/neondb?sslmode=require
NEXTAUTH_SECRET=generate-with-openssl-rand-base64-32
NEXTAUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_PRO_PRICE_ID=price_...

スキャフォールドのチェックリスト

以下のフェーズを順番に完了する必要があります。次に進む前に、各フェーズの最後に検証してください。

フェーズ 1 — 基盤
  • 1. TypeScript と App Router を使用して Next.js を初期化する
  • 2. カスタムテーマトークンを使って Tailwind CSS を設定する
  • 3. shadcn/ui をインストールして設定する
  • 4. ESLint + Prettier を設定する
  • 5. 必要なすべての変数を記載した .env.example を作成する

✅ 検証: npm run build を実行します。TypeScript または lint のエラーが表示されないことを確認してください。
🔧 ビルドに失敗した場合: tsconfig.json のパスと、shadcn/ui のすべてのピア依存関係がインストールされていることを確認してください。

フェーズ 2 — データベース
  • 6. Drizzle ORM をインストールして設定する
  • 7. スキーマを記述する(users、accounts、sessions、verification_tokens)
  • 8. 初期マイグレーションを生成して適用する
  • 9. DB クライアントのシングルトンを lib/db.ts からエクスポートする
  • 10. ローカル環境で DB 接続をテストする

✅ 検証: テストスクリプトで db.select().from(users) を実行します。例外が発生せず、空の配列が返ることを確認してください。
🔧 DB 接続に失敗した場合: DATABASE_URL の形式に NeonDB/Supabase 用の ?sslmode=require が含まれていることを確認してください。drizzle-kit push(開発環境)または drizzle-kit migrate(本番環境)でマイグレーションが適用されていることを確認してください。

フェーズ 3 — 認証
  • 11. 認証プロバイダーをインストールする(NextAuth / Clerk / Supabase)
  • 12. OAuth プロバイダーを設定する(Google / GitHub)
  • 13. 認証 API ルートを作成する
  • 14. セッションコールバックでユーザー ID とサブスクリプションのステータスを追加する
  • 15. ミドルウェアでダッシュボードのルートを保護する
  • 16. エラー状態を含むログインページと登録ページを構築する

✅ 検証: OAuth でサインインし、セッションユーザーに id と subscriptionStatus が含まれていることを確認します。セッションなしで /dashboard にアクセスし、/login にリダイレクトされることを確認してください。
🔧 本番環境でサインアウトのループが発生する場合: NEXTAUTH_SECRET が設定され、デプロイ間で一貫していることを確認してください。TypeScript エラーが発生する場合は、セッション型を拡張するために declare module "next-auth" を追加してください。

フェーズ 4 — 決済
  • 17. TypeScript 型を使って Stripe クライアントを初期化する
  • 18. チェックアウトセッションのルートを作成する
  • 19. カスタマーポータルのルートを作成する
  • 20. 署名検証を行う Stripe webhook ハンドラーを作成する
  • 21. webhook でユーザーのサブスクリプションステータスを DB 内で冪等に更新する

✅ 検証: 4242 4242 4242 4242 のカードを使用して Stripe のテストチェックアウトを完了します。stripeSubscriptionId が DB に書き込まれていることを確認してください。checkout.session.completed webhook イベントを再実行し、冪等性(DB への書き込みが重複しないこと)を確認してください。
🔧 webhook の署名検証に失敗する場合: ローカルで stripe listen --forward-to localhost:3000/api/webhooks/stripe を使用してください。webhook シークレットを生の値でハードコードしないでください。STRIPE_WEBHOOK_SECRET がリスナーの出力と一致していることを確認してください。

フェーズ 5 — UI
  • 22. ヒーロー、機能、料金の各セクションを含むランディングページ
  • 23. サイドバーとレスポンシブヘッダーを備えたダッシュボードレイアウト
  • 24. 現在のプランとアップグレードオプションを表示する請求ページ
  • 25. プロフィール更新フォームと成功状態を備えた設定ページ

✅ 検証: 最終的な本番ビルドを確認するため、npm run build を実行します。すべてのルートを手動で開き、レイアウトの崩れ、セッションデータの欠落、水和エラーがないことを確認してください。


リファレンスファイル

追加のガイダンスとして、スキャフォールドとともに次の補足リファレンスファイルを生成してください。

  • CUSTOMIZATION.md — 認証プロバイダー、データベースの選択肢、ORM の代替案、決済プロバイダー、UI テーマ、課金モデル(ユーザーごと、定額、従量課金)。
  • PITFALLS.md — よくある失敗要因:NEXTAUTH_SECRET の未設定、Webhook シークレットの不一致、Drizzle と Edge ランタイムの競合、セッション型の拡張漏れ、開発環境と本番環境でのマイグレーション戦略の違い。
  • BEST_PRACTICES.md — Stripe のシングルトンパターン、フォームの変更処理にサーバーアクションを使用する方法、冪等な Webhook ハンドラー、非同期のダッシュボードデータに対する Suspense 境界、stripeCurrentPeriodEnd を使ったサーバー側の機能制限、Upstash Redis と @upstash/ratelimit を使った認証ルートのレート制限。

コメント