关于 nextjs-supabase-saas-planner
面向使用 Next.js 15 和 Supabase 的 SaaS 团队,将产品构想整理为技术计划,涵盖架构、数据、认证、计费和分阶段路线图。
主要功能
- 01架构与技术栈规划
- 02数据库模式及 RLS 模式
- 03认证与多租户方案
- 04计费集成规划
- 05MVP 优先级与上线路线图
适用场景
- 01将 SaaS 构想转为技术规格
- 02规划 Supabase 数据模型与访问策略
- 03设计基于组织的租户隔离
- 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.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:
- 使用服务器组件提升性能
- 使用服务器操作执行变更
- 内置缓存策略
- 出色的开发体验和快速迭代
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、魔法链接、MFABILLING_PATTERNS.md- Stripe 集成、Webhook、计量MULTI_TENANT_PATTERNS.md- 组织结构、数据隔离DEPLOYMENT_STRATEGIES.md- CI/CD、环境、监控
示例输出格式
规划 SaaS 时,请提供:
-
执行摘要
- 产品概述
- 目标用户
- 主要功能
-
技术架构
- 系统图
- 技术栈选型依据
- 数据库架构
-
文件结构
- 完整的文件夹组织结构
- 关键文件说明
-
开发路线图
- 按周细分
- 功能优先级排序
- 预计工作量
-
部署计划
- 基础设施设置
- 环境配置
- 上线清单
-
后续步骤
- 立即执行的操作
- 设置命令
- 文档链接
成功标准
规划完善的 SaaS 应具备:
- ✅ 清晰且配置了 RLS 的数据库架构
- ✅ 安全的身份验证流程
- ✅ 可扩展的架构
- ✅ 可正常使用的账单集成
- ✅ 有序的文件结构
- ✅ 切实可行的时间安排
- ✅ 明确的 MVP 范围
- ✅ 部署策略
评论
支持 Markdown