mcpField
N
nextjs-supabase-saas-planner

nextjs-supabase-saas-planner

github.com/sitechfromgeorgia开发36 次安装
代码仓库

关于 nextjs-supabase-saas-planner

面向使用 Next.js 15 和 Supabase 的 SaaS 团队,将产品构想整理为技术计划,涵盖架构、数据、认证、计费和分阶段路线图。

主要功能

  1. 01架构与技术栈规划
  2. 02数据库模式及 RLS 模式
  3. 03认证与多租户方案
  4. 04计费集成规划
  5. 05MVP 优先级与上线路线图

适用场景

  1. 01将 SaaS 构想转为技术规格
  2. 02规划 Supabase 数据模型与访问策略
  3. 03设计基于组织的租户隔离
  4. 04确定 MVP 范围与发布里程碑

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/sitechfromgeorgia/georgian-distribution-system --skill nextjs-supabase-saas-planner

安装后在 Claude Code 中输入/nextjs-supabase-saas-planner确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
references/
AUTHENTICATION_PATTERNS.md
BILLING_PATTERNS.md

详细介绍

Next.js + Supabase SaaS 规划助手

概览

专业的 SaaS 规划助手,可将产品创意转化为可执行的技术路线图。专注于 Next.js 15 + Supabase 技术栈,涵盖架构设计、数据库架构、身份验证模式、多租户、账单集成和发布策略。

适用场景:

  • 从零开始规划新的 SaaS 产品
  • 将产品创意转化为技术规范
  • 为 SaaS 应用设计数据库架构
  • 设置身份验证和授权
  • 实施多租户架构
  • 集成订阅账单(Stripe、Paddle)
  • 规划功能发布和 MVP 优先级
  • 制定开发路线图和时间表

规划工作流

阶段 1:发现需求并明确要求

首先询问以下问题:

  1. 产品概览

    • 你的 SaaS 解决什么问题?
    • 目标用户是谁?
    • 你的独特价值主张是什么?
  2. 规模预期

    • 第一年预计有多少用户?(0-1K、1K-10K、10K-100K、100K+)
    • 预期增长率是多少?
    • 用户的地理分布如何?
  3. 团队和时间表

    • 团队规模和技能如何?
    • 发布计划是什么?(MVP 日期)
    • 有哪些预算限制?
  4. 商业模式

    • 定价层级是什么?
    • 提供免费试用还是免费增值模式?
    • 哪些功能可以区分付费层级?
  5. 核心功能

    • MVP 必须包含哪些功能?
    • v1.1 值得添加哪些功能?
    • 对未来路线图有哪些想法?
阶段 2:技术架构规划

根据需求,创建:

  1. 系统架构图
  2. 数据库架构
  3. 文件/文件夹结构
  4. 身份验证流程
  5. 多租户策略(如适用)
  6. 账单集成计划
  7. 部署架构
阶段 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:

  • 使用服务器组件提升性能
  • 使用服务器操作执行变更
  • 内置缓存策略
  • 出色的开发体验和快速迭代

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 Webhook
  • 配置域名
  • 启用 SSL 证书
  • 设置分析工具
  • 错误跟踪(Sentry)
  • 配置电子邮件服务
  • 服务条款 + 隐私政策页面
上线后
  • 监控错误率
  • 检查订阅流程
  • 测试 Webhook 投递
  • 监控数据库性能
  • 设置备份
  • 创建状态页面
  • 实施功能开关

最佳实践

性能
  • 默认使用 Server Components
  • 实施适当的缓存策略
  • 使用 next/image 优化图片
  • 使用 React Server Actions 执行变更
  • 延迟加载大型组件
安全
  • 始终使用 RLS 策略
  • 在服务器端验证输入
  • 切勿向客户端暴露服务角色密钥
  • 使用预处理语句(Supabase 会处理)
  • 为敏感端点实施速率限制
开发
  • 严格使用 TypeScript
  • 为关键路径编写测试
  • 使用数据库迁移(切勿手动更改)
  • 为 API 进行版本控制
  • 为复杂逻辑编写文档
用户体验
  • 在所有地方添加加载状态
  • 实施乐观更新
  • 显示清晰的错误消息
  • 让引导流程顺畅
  • 添加有帮助的空状态

常见模式参考

如需了解详细的实现模式,请参阅参考文档:

  • AUTHENTICATION_PATTERNS.md - OAuth、魔法链接、MFA
  • BILLING_PATTERNS.md - Stripe 集成、Webhook、计量
  • MULTI_TENANT_PATTERNS.md - 组织结构、数据隔离
  • DEPLOYMENT_STRATEGIES.md - CI/CD、环境、监控

示例输出格式

规划 SaaS 时,请提供:

  1. 执行摘要

    • 产品概述
    • 目标用户
    • 主要功能
  2. 技术架构

    • 系统图
    • 技术栈选型依据
    • 数据库架构
  3. 文件结构

    • 完整的文件夹组织结构
    • 关键文件说明
  4. 开发路线图

    • 按周细分
    • 功能优先级排序
    • 预计工作量
  5. 部署计划

    • 基础设施设置
    • 环境配置
    • 上线清单
  6. 后续步骤

    • 立即执行的操作
    • 设置命令
    • 文档链接

成功标准

规划完善的 SaaS 应具备:

  • ✅ 清晰且配置了 RLS 的数据库架构
  • ✅ 安全的身份验证流程
  • ✅ 可扩展的架构
  • ✅ 可正常使用的账单集成
  • ✅ 有序的文件结构
  • ✅ 切实可行的时间安排
  • ✅ 明确的 MVP 范围
  • ✅ 部署策略

评论

支持 Markdown