nextjs-supabase-saas-planner について
Next.js 15 と Supabase を使うチーム向けに、SaaS の構想を技術計画にまとめるスキルです。アーキテクチャ、データ、認証、課金、段階的なロードマップを扱います。
主な機能
- 01アーキテクチャと技術スタックの計画
- 02データベーススキーマと RLS パターン
- 03認証とマルチテナント設計
- 04課金連携の計画
- 05MVP の優先順位とローンチロードマップ
活用シーン
- 01SaaS の構想を技術仕様に変換
- 02Supabase のデータモデルとアクセス制御を計画
- 03組織単位のテナント分離を設計
- 04MVP の範囲とリリース節目を決定
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ sitechfromgeorgia/ georgian-distribution-system --skill nextjs-supabase-saas-plannerインストール後、Claude Code で/nextjs-supabase-saas-plannerを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.mdreferences/AUTHENTICATION_PATTERNS.mdBILLING_PATTERNS.md詳細
Next.js + Supabase SaaS プランナー
概要
プロダクトのアイデアを実行可能な技術ロードマップに変換する、SaaS 計画のエキスパートアシスタントです。Next.js 15 + Supabase スタックを専門とし、アーキテクチャ設計、データベーススキーマ、認証パターン、マルチテナンシー、請求の統合、ローンチ戦略を扱います。
このスキルを使用する場面:
- 新しい SaaS プロダクトをゼロから計画するとき
- プロダクトのアイデアを技術仕様に落とし込むとき
- SaaS アプリケーションのデータベーススキーマを設計するとき
- 認証と認可を設定するとき
- マルチテナントアーキテクチャを実装するとき
- サブスクリプション請求(Stripe、Paddle)を統合するとき
- 機能のリリースと MVP の優先順位を計画するとき
- 開発ロードマップとスケジュールを作成するとき
計画ワークフロー
フェーズ 1:要件の確認と整理
まず、次の質問をします:
-
プロダクトの概要
- SaaS はどのような問題を解決しますか?
- ターゲットユーザーは誰ですか?
- 独自の価値提案は何ですか?
-
想定規模
- 初年度のユーザー数はどのくらいですか?(0~1K、1K~10K、10K~100K、100K+)
- 想定される成長率はどのくらいですか?
- 地理的な分布はどうなりますか?
-
チームとスケジュール
- チームの人数とスキルは?
- ローンチの予定は?(MVP の日付)
- 予算の制約はありますか?
-
ビジネスモデル
- 料金プランは?
- 無料トライアルですか、それともフリーミアムですか?
- 有料プランを差別化する機能は何ですか?
-
コア機能
- MVP に必須の機能は?
- v1.1 であると望ましい機能は?
- 今後のロードマップに含めるアイデアは?
フェーズ 2:技術アーキテクチャの計画
要件に基づいて、以下を作成します:
- システムアーキテクチャ図
- データベーススキーマ
- ファイル/フォルダー構成
- 認証フロー
- マルチテナンシー戦略(該当する場合)
- 請求統合計画
- デプロイアーキテクチャ
フェーズ 3:開発ロードマップ
週ごとの計画を作成します:
- 第 1~2 週:セットアップとインフラストラクチャ
- 第 3~4 週:コア機能
- 第 5~6 週:認証と請求
- 第 7~8 週:仕上げとテスト
- 第 9 週:ローンチ準備
Next.js 15 + Supabase スタック(2025)
推奨技術スタック
Frontend:
├─ Next.js 15 (App Router)
├─ React 19
├─ TypeScript
├─ Tailwind CSS
└─ shadcn/ui components
Backend:
├─ Next.js API Routes (Server Actions)
├─ Supabase Edge Functions (when needed)
└─ Stripe/Paddle webhooks
Database:
├─ PostgreSQL (Supabase)
├─ Row Level Security (RLS)
└─ Supabase Realtime (optional)
Authentication:
├─ Supabase Auth
├─ OAuth providers (Google, GitHub, etc.)
└─ Magic links
Storage:
└─ Supabase Storage (files, images)
Deployment:
├─ Vercel (frontend + API)
├─ Supabase (database, auth, storage)
└─ Cloudflare (CDN, DNS)
Payments:
├─ Stripe (recommended)
└─ Paddle (alternative)
Monitoring:
├─ Vercel Analytics
├─ Sentry (error tracking)
└─ PostHog (product analytics - optional)
このスタックを選ぶ理由
Next.js 15:
- パフォーマンスを高めるサーバーコンポーネント
- データ更新に使えるサーバーアクション
- 組み込みのキャッシュ戦略
- 優れた DX と迅速な反復開発
Supabase:
- すぐに使える REST API
- リアルタイムサブスクリプション
- 組み込みの認証機能
- 行レベルセキュリティ
- オープンソース(ベンダーロックインのリスクなし)
TypeScript:
- 型安全性によるバグの削減
- IDE のサポート向上
- コードの自己文書化
Stripe:
- 決済業界の標準
- 優れたドキュメント
- サブスクリプション管理
- 請求書の処理
ファイル構成テンプレート
my-saas/
├─ app/
│ ├─ (auth)/
│ │ ├─ login/
│ │ │ └─ page.tsx
│ │ ├─ signup/
│ │ │ └─ page.tsx
│ │ ├─ forgot-password/
│ │ │ └─ page.tsx
│ │ └─ layout.tsx
│ │
│ ├─ (marketing)/
│ │ ├─ page.tsx # Landing page
│ │ ├─ pricing/
│ │ │ └─ page.tsx
│ │ ├─ about/
│ │ │ └─ page.tsx
│ │ └─ layout.tsx
│ │
│ ├─ (dashboard)/
│ │ ├─ dashboard/
│ │ │ └─ page.tsx
│ │ ├─ settings/
│ │ │ ├─ profile/
│ │ │ │ └─ page.tsx
│ │ │ ├─ billing/
│ │ │ │ └─ page.tsx
│ │ │ └─ team/
│ │ │ └─ page.tsx
│ │ └─ layout.tsx
│ │
│ ├─ api/
│ │ ├─ webhooks/
│ │ │ └─ stripe/
│ │ │ └─ route.ts
│ │ └─ og/ # Open Graph images
│ │ └─ route.tsx
│ │
│ ├─ layout.tsx # Root layout
│ └─ globals.css
│
├─ components/
│ ├─ ui/ # shadcn components
│ │ ├─ button.tsx
│ │ ├─ card.tsx
│ │ ├─ dialog.tsx
│ │ └─ ...
│ ├─ auth/
│ │ ├─ login-form.tsx
│ │ └─ signup-form.tsx
│ ├─ dashboard/
│ │ ├─ sidebar.tsx
│ │ └─ navbar.tsx
│ ├─ marketing/
│ │ ├─ hero.tsx
│ │ ├─ features.tsx
│ │ └─ pricing-cards.tsx
│ └─ shared/
│ ├─ user-avatar.tsx
│ └─ loading-spinner.tsx
│
├─ lib/
│ ├─ supabase/
│ │ ├─ client.ts # Client-side Supabase
│ │ ├─ server.ts # Server-side Supabase
│ │ └─ middleware.ts # Auth middleware
│ ├─ stripe/
│ │ ├─ client.ts
│ │ └─ server.ts
│ ├─ db/
│ │ └─ queries.ts # Database queries
│ ├─ auth.ts # Auth helpers
│ └─ utils.ts # General utilities
│
├─ hooks/
│ ├─ use-user.ts # User session hook
│ ├─ use-subscription.ts # Subscription status
│ └─ use-toast.ts
│
├─ actions/ # Server Actions
│ ├─ auth/
│ │ ├─ login.ts
│ │ ├─ signup.ts
│ │ └─ logout.ts
│ ├─ billing/
│ │ ├─ create-checkout.ts
│ │ └─ cancel-subscription.ts
│ └─ profile/
│ └─ update-profile.ts
│
├─ types/
│ ├─ database.ts # Generated from Supabase
│ ├─ supabase.ts
│ └─ stripe.ts
│
├─ supabase/
│ ├─ migrations/ # Database migrations
│ │ └─ 20250101000000_initial.sql
│ ├─ functions/ # Edge Functions (optional)
│ └─ config.toml
│
├─ public/
│ ├─ images/
│ └─ icons/
│
├─ middleware.ts # Next.js middleware (auth)
├─ next.config.js
├─ tailwind.config.js
├─ tsconfig.json
├─ package.json
└─ .env.local
データベーススキーマのパターン
SaaS の基本テーブル
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Users table (extended from Supabase auth.users)
CREATE TABLE profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
email TEXT UNIQUE NOT NULL,
full_name TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Organizations/Teams (multi-tenant)
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
logo_url TEXT,
subscription_tier TEXT DEFAULT 'free',
subscription_status TEXT DEFAULT 'active',
stripe_customer_id TEXT UNIQUE,
stripe_subscription_id TEXT UNIQUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Organization memberships
CREATE TABLE organization_members (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member', -- 'owner', 'admin', 'member'
invited_by UUID REFERENCES profiles(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(organization_id, user_id)
);
-- Subscription plans
CREATE TABLE plans (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
description TEXT,
price_monthly DECIMAL(10,2),
price_yearly DECIMAL(10,2),
stripe_price_id_monthly TEXT,
stripe_price_id_yearly TEXT,
features JSONB,
limits JSONB, -- { "users": 5, "projects": 10 }
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Subscription usage tracking
CREATE TABLE usage_records (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
metric TEXT NOT NULL, -- 'api_calls', 'storage_mb', 'users'
value INTEGER NOT NULL,
period_start DATE NOT NULL,
period_end DATE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Invitations
CREATE TABLE invitations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
email TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member',
token TEXT UNIQUE NOT NULL,
invited_by UUID REFERENCES profiles(id),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
accepted_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Audit log (optional but recommended)
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id),
user_id UUID REFERENCES profiles(id),
action TEXT NOT NULL,
resource_type TEXT,
resource_id UUID,
metadata JSONB,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_org_members_org ON organization_members(organization_id);
CREATE INDEX idx_org_members_user ON organization_members(user_id);
CREATE INDEX idx_usage_org_period ON usage_records(organization_id, period_start, period_end);
CREATE INDEX idx_invitations_token ON invitations(token);
CREATE INDEX idx_audit_logs_org ON audit_logs(organization_id);
行レベルセキュリティ(RLS)ポリシー
-- Enable RLS
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
-- Profiles: Users can read and update their own profile
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
-- Organizations: Members can read their organizations
CREATE POLICY "Members can view their organizations"
ON organizations FOR SELECT
USING (
id IN (
SELECT organization_id
FROM organization_members
WHERE user_id = auth.uid()
)
);
-- Only owners can update organizations
CREATE POLICY "Owners can update organizations"
ON organizations FOR UPDATE
USING (
id IN (
SELECT organization_id
FROM organization_members
WHERE user_id = auth.uid() AND role = 'owner'
)
);
-- Organization members: Members can view other members
CREATE POLICY "Members can view team members"
ON organization_members FOR SELECT
USING (
organization_id IN (
SELECT organization_id
FROM organization_members
WHERE user_id = auth.uid()
)
);
認証の設定
Supabase Auth の設定
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr';
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
// lib/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
cookieStore.set({ name, value, ...options });
},
remove(name: string, options: CookieOptions) {
cookieStore.set({ name, value: '', ...options });
},
},
}
);
}
認証ミドルウェア
// middleware.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
response.cookies.set({
name,
value,
...options,
});
},
remove(name: string, options: CookieOptions) {
response.cookies.set({
name,
value: '',
...options,
});
},
},
}
);
const { data: { user } } = await supabase.auth.getUser();
// Redirect to login if not authenticated
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Redirect to dashboard if already authenticated
if (user && (
request.nextUrl.pathname === '/login' ||
request.nextUrl.pathname === '/signup'
)) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return response;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};
Stripe の統合
セットアップ
// lib/stripe/server.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
});
// Create checkout session
export async function createCheckoutSession(
organizationId: string,
priceId: string,
userId: string
) {
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/billing?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
client_reference_id: organizationId,
metadata: {
organizationId,
userId,
},
});
return session;
}
Webhook ハンドラー
// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { stripe } from '@/lib/stripe/server';
import { createClient } from '@/lib/supabase/server';
export async function POST(req: Request) {
const body = await req.text();
const signature = (await headers()).get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
const supabase = await createClient();
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object as Stripe.Checkout.Session;
await supabase
.from('organizations')
.update({
stripe_customer_id: session.customer as string,
stripe_subscription_id: session.subscription as string,
subscription_status: 'active',
})
.eq('id', session.metadata?.organizationId);
break;
case 'customer.subscription.updated':
const subscription = event.data.object as Stripe.Subscription;
await supabase
.from('organizations')
.update({
subscription_status: subscription.status,
})
.eq('stripe_subscription_id', subscription.id);
break;
case 'customer.subscription.deleted':
const deletedSub = event.data.object as Stripe.Subscription;
await supabase
.from('organizations')
.update({
subscription_status: 'canceled',
subscription_tier: 'free',
})
.eq('stripe_subscription_id', deletedSub.id);
break;
}
return NextResponse.json({ received: true });
}
マルチテナンシーのパターン
組織ベース(推奨)
パターン: 各組織がそれぞれ独自のデータ領域を持ちます。
メリット:
- データを明確に分離できる
- 実装が容易
- 組織ごとにスケーリングできる
- 組織ごとの請求がシンプル
実装:
// All queries include organization_id
const { data } = await supabase
.from('projects')
.select('*')
.eq('organization_id', currentOrgId);
サブドメインベース(上級)
パターン: 各組織が独自のサブドメインを持ちます(例:acme.yourapp.com)。
メリット:
- ブランディングの向上
- 明確な分離
- プロフェッショナルな外観
課題:
- DNS の管理
- SSL 証明書
- ルーティングの複雑化
実装:
// middleware.ts
export function middleware(request: NextRequest) {
const hostname = request.headers.get('host')!;
const subdomain = hostname.split('.')[0];
// Fetch organization by subdomain
// Set in request context
}
MVP 機能の優先順位付け
第 1~2 週:基盤
- プロジェクトのセットアップ(Next.js + Supabase)
- データベーススキーマの作成
- 認証(メール + OAuth)
- 基本的なレイアウトとルーティング
- ランディングページ
第 3~4 週:コア機能
- ダッシュボードのレイアウト
- 最初のコア機能(主な価値提案)
- ユーザープロフィールの管理
- 組織/チームの作成
- チームメンバーの招待
第 5~6 週:収益化
- 料金ページ
- Stripe の統合
- サブスクリプション管理
- 利用制限の適用
- 請求ページ
第 7~8 週:仕上げ
- メール通知
- エラー処理
- ローディング状態
- レスポンシブデザイン
- SEO の最適化
第 9 週:リリース準備
- テスト(E2E、ユニット)
- セキュリティ監査
- パフォーマンスの最適化
- ドキュメント
- デプロイスクリプト
環境変数
# .env.local
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-key
# Stripe
STRIPE_SECRET_KEY=sk_test_xxxxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Optional
SENTRY_DSN=your-sentry-dsn
POSTHOG_KEY=your-posthog-key
デプロイチェックリスト
リリース前
- 環境変数の設定
- データベースのマイグレーションの実行
- RLS ポリシーのテスト
- Stripe ウェブフックの設定
- ドメインの設定
- SSL 証明書の有効化
- アナリティクスの設定
- エラートラッキング(Sentry)
- メールサービスの設定
- 利用規約とプライバシーポリシーのページ
リリース後
- エラー率の監視
- サブスクリプションのフローを確認
- ウェブフックの配信をテスト
- データベースのパフォーマンスを監視
- バックアップの設定
- ステータスページの作成
- フィーチャーフラグの実装
ベストプラクティス
パフォーマンス
- デフォルトで Server Components を使用する
- 適切なキャッシュ戦略を実装する
next/imageで画像を最適化する- ミューテーションには React Server Actions を使用する
- 負荷の高いコンポーネントは遅延読み込みする
セキュリティ
- 常に RLS ポリシーを使用する
- サーバー側で入力を検証する
- サービスロールキーをクライアントに公開しない
- プリペアドステートメントを使用する(Supabase が処理します)
- 機密性の高いエンドポイントにレート制限を実装する
開発
- TypeScript を厳格に使用する
- 重要な処理経路のテストを作成する
- データベースのマイグレーションを使用する(手動で変更しない)
- API のバージョンを管理する
- 複雑なロジックを文書化する
ユーザーエクスペリエンス
- あらゆる場所にローディング状態を追加する
- 楽観的更新を実装する
- わかりやすいエラーメッセージを表示する
- スムーズなオンボーディングを実現する
- 役立つ空状態を追加する
よくあるパターンのリファレンス
詳しい実装パターンについては、以下のリファレンスドキュメントを参照してください。
AUTHENTICATION_PATTERNS.md- OAuth、マジックリンク、MFABILLING_PATTERNS.md- Stripe の統合、ウェブフック、従量課金MULTI_TENANT_PATTERNS.md- 組織構造、データ分離DEPLOYMENT_STRATEGIES.md- CI/CD、環境、監視
出力形式の例
SaaS を計画する際は、以下を提示してください。
-
エグゼクティブサマリー
- 製品の概要
- 対象ユーザー
- 主な機能
-
技術アーキテクチャ
- システム図
- 技術スタックを選定した理由
- データベーススキーマ
-
ファイル構成
- フォルダー構成の全体像
- 主要ファイルの説明
-
開発ロードマップ
- 週ごとの内訳
- 機能の優先順位付け
- 見積もり工数
-
デプロイ計画
- インフラストラクチャの設定
- 環境設定
- リリースチェックリスト
-
次のステップ
- すぐに行う作業
- セットアップコマンド
- ドキュメントへのリンク
成功基準
適切に計画された SaaS には、以下が必要です。
- ✅ RLS を含む明確なデータベーススキーマ
- ✅ 安全な認証フロー
- ✅ スケーラブルなアーキテクチャ
- ✅ 動作する課金統合
- ✅ 整理されたファイル構成
- ✅ 現実的なスケジュール
- ✅ 明確な MVP の範囲
- ✅ デプロイ戦略
コメント
Markdown 対応