本文演示如何在 Vercel 上从零部署一个 Next.js + Postgres 的多租户 SaaS 应用:使用 Next.js App Router 做前后端、Prisma 做 ORM、云 Postgres(Neon / Supabase / Vercel Postgres 均可)做数据底座,实现 workspace 租户隔离的数据模型与 REST API,并通过 Git 推送一键上线。对 Vercel 平台本身不熟悉的读者,建议先阅读 Vercel 详解:前端与 AI 应用的一站式云平台。
0. 目标 & 架构概览
我们做一个极简 SaaS Demo:
技术栈:
- Next.js 14+(App Router)
- Postgres(云服务:Neon / Supabase / Vercel Postgres 均可)
- Prisma ORM
功能(极简版):
workspace(租户)表:
workspaces用户表:
users项目表:
projects(挂在 workspace 下)提供几个 API:
- 创建 workspace
- 在指定 workspace 下创建 project
- 查询某个 workspace 下的 project 列表
架构思路:
所有业务表都带
workspace_id(或tenant_id)→ 多租户基础。Next.js API Route(或 Route Handler)里,从请求头 / 路径解析当前 workspace,然后查询时带上
where: { workspaceId }即可。部署时:
- Next.js 前后端都扔到 Vercel
- Postgres 单独用一个云服务,暴露
DATABASE_URL,在 Vercel 的环境变量里配置。
1. 初始化 Next.js 项目
在本地建项目(假设目录名 saas-demo):
npx create-next-app@latest saas-demo \
--typescript \
--eslint \
--app \
--src-dir \
--import-alias "@/*"
进入目录:
cd saas-demo
本地先启动一下看是否正常:
npm run dev
# 或
pnpm dev
浏览器访问 http://localhost:3000,确认项目 OK。
2. 准备 Postgres(本地 or 云)
你可以选任意云 Postgres,这里用一个通用思路:
- 去 Neon / Supabase / Railway / Vercel Postgres 创建一个 Postgres 实例。
- 拿到一个标准的连接串,形如:
postgresql://USER:PASSWORD@HOST:PORT/DB_NAME?schema=public
先记下来,后面要塞进 .env 和 Vercel。
本地开发用 .env:
cp .env.example .env # 如果有
# 或直接创建 .env
写入:
DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/DB_NAME?schema=public"
提示:
- 本地可以用 docker 跑一个 Postgres,线上换成云服务,只要
DATABASE_URL一样即可。- Vercel 部署时再在项目的 Environment 里填同样的
DATABASE_URL。
3. 接入 Prisma & 定义 SaaS 数据模型
安装 Prisma:
npm install prisma --save-dev
npm install @prisma/client
初始化 Prisma:
npx prisma init
这会生成 prisma/schema.prisma 和 .env,逻辑类似。
修改 prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// 多租户基础:Workspace + User + Project
model Workspace {
id String @id @default(cuid())
name String
slug String @unique // 用于 URL / 子域
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users UserWorkspace[]
projects Project[]
}
model User {
id String @id @default(cuid())
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
workspaces UserWorkspace[]
}
model UserWorkspace {
id String @id @default(cuid())
user User @relation(fields: [userId], references: [id])
userId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
workspaceId String
role String // owner / admin / member
createdAt DateTime @default(now())
}
model Project {
id String @id @default(cuid())
name String
description String?
workspace Workspace @relation(fields: [workspaceId], references: [id])
workspaceId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
然后执行迁移,把表建到 Postgres:
npx prisma migrate dev --name init_saas_schema
本地成功后,你可以用 npx prisma studio 看下数据结构。
4. Prisma Client 封装(避免热重载多实例)
在 src/lib/prisma.ts 中创建单例(Next.js 热重载时防止多次实例化):
// src/lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = global as unknown as { prisma: PrismaClient | undefined };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: ["query", "error", "warn"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
5. 简单的 API:按 workspace 创建 & 列出项目
我们假设用路径 /api/workspaces/[slug]/projects:
POST: 在某个 workspace 下创建一个 projectGET: 获取该 workspace 下的全部 projects
在 App Router 下创建:
src/app/api/workspaces/[slug]/projects/route.ts:
// src/app/api/workspaces/[slug]/projects/route.ts
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
// 简单的帮助函数:根据 workspace slug 获取 workspace
async function getWorkspaceBySlug(slug: string) {
return prisma.workspace.findUnique({
where: { slug },
});
}
export async function GET(
req: NextRequest,
{ params }: { params: { slug: string } }
) {
const { slug } = params;
const workspace = await getWorkspaceBySlug(slug);
if (!workspace) {
return NextResponse.json(
{ error: "Workspace not found" },
{ status: 404 }
);
}
const projects = await prisma.project.findMany({
where: { workspaceId: workspace.id },
orderBy: { createdAt: "desc" },
});
return NextResponse.json({ projects });
}
export async function POST(
req: NextRequest,
{ params }: { params: { slug: string } }
) {
const { slug } = params;
const workspace = await getWorkspaceBySlug(slug);
if (!workspace) {
return NextResponse.json(
{ error: "Workspace not found" },
{ status: 404 }
);
}
const body = await req.json().catch(() => null) as {
name?: string;
description?: string;
};
if (!body?.name) {
return NextResponse.json(
{ error: "name is required" },
{ status: 400 }
);
}
const project = await prisma.project.create({
data: {
name: body.name,
description: body.description ?? null,
workspaceId: workspace.id,
},
});
return NextResponse.json({ project }, { status: 201 });
}
再给一个创建 workspace 的简单接口:src/app/api/workspaces/route.ts:
// src/app/api/workspaces/route.ts
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(req: NextRequest) {
const body = await req.json().catch(() => null) as {
name?: string;
slug?: string;
};
if (!body?.name || !body?.slug) {
return NextResponse.json(
{ error: "name and slug are required" },
{ status: 400 }
);
}
const workspace = await prisma.workspace.create({
data: {
name: body.name,
slug: body.slug,
},
});
return NextResponse.json({ workspace }, { status: 201 });
}
这样你就有了最基本的多租户数据结构 + API。
6. 在页面里简单调用(前端 Demo)
例如在 src/app/[slug]/page.tsx 里根据 workspace slug 展示项目列表:
// src/app/[slug]/page.tsx
import { prisma } from "@/lib/prisma";
interface Props {
params: { slug: string };
}
export default async function WorkspacePage({ params }: Props) {
const workspace = await prisma.workspace.findUnique({
where: { slug: params.slug },
});
if (!workspace) {
return <div>Workspace not found</div>;
}
const projects = await prisma.project.findMany({
where: { workspaceId: workspace.id },
orderBy: { createdAt: "desc" },
});
return (
<main style={{ padding: 24 }}>
<h1>Workspace: {workspace.name}</h1>
<h2>Projects</h2>
<ul>
{projects.map((p) => (
<li key={p.id}>
<strong>{p.name}</strong>
{p.description && <span> — {p.description}</span>}
</li>
))}
</ul>
</main>
);
}
注意:这个页面是 服务器组件,直接在服务端用 Prisma 查询。
后面你可以逐步换成 Client 组件 + API 调用 + 状态管理等等。
7. 为 Vercel 部署做准备
7.1 Git 仓库
初始化 git 并推到 GitHub(或 GitLab / Bitbucket):
git init
git add .
git commit -m "Init SaaS demo"
git remote add origin git@github.com:yourname/saas-demo.git
git push -u origin main
7.2 Vercel 上创建项目
打开 Vercel Dashboard,点击 New Project。
选择刚刚的 Git 仓库
saas-demo。Vercel 会自动识别这是 Next.js App Router 项目,构建命令一般为:
- Install command:
npm install - Build command:
npm run build - Output dir:
.next
- Install command:
可以保持默认。
7.3 配置环境变量
在 Vercel 项目设置的 Environment Variables 里配置:
DATABASE_URL= 刚才的 Postgres 连接串
建议:
- 在 Production 和 Preview 环境都配置同样的变量,或者至少 Production 先配好。
- 如果你需要区分 dev / prod 数据库,就用 Vercel 的
Preview环境指向测试 DB,Production环境指向正式 DB。
7.4 运行时注意:Node.js 环境
Prisma / Postgres 需要 Node.js runtime,不要放到 Edge runtime 中。
- App Router 的 page / layout 默认是 Node runtime(Server Components)。
- Route Handler 默认也是 Node runtime,只要你没配置
export const runtime = "edge"就行。 - 如果之后你要用 Edge 中间件,就注意不要在 Edge runtime 直接使用 Prisma。
8. 首次部署 & 验证
配置好之后,在 Vercel 上点击 Deploy:
- 构建完成后,会得到一个生产地址,例如:
https://saas-demo-yourname.vercel.app - 你可以用 Postman / curl 测试:
# 1. 创建 workspace
curl -X POST https://saas-demo-yourname.vercel.app/api/workspaces \
-H "Content-Type: application/json" \
-d '{"name":"Acme Corp", "slug":"acme"}'
# 2. 在 acme workspace 下创建 project
curl -X POST https://saas-demo-yourname.vercel.app/api/workspaces/acme/projects \
-H "Content-Type: application/json" \
-d '{"name":"First Project","description":"Hello SaaS"}'
# 3. 获取 acme workspace 下项目
curl https://saas-demo-yourname.vercel.app/api/workspaces/acme/projects
浏览器访问:
https://saas-demo-yourname.vercel.app/acme
就能看到刚才添加的项目列表。
成本估算:这个 Demo 要花多少钱?
在上线前了解账单预期很重要。下面按 Vercel 计划 + Postgres 云服务分别估算:
Vercel 账单
| 项目 | Hobby(个人) | Pro(商用) | 说明 |
|---|---|---|---|
| 月费 | $0 | ~$20/用户 | Pro 包含 1TB 流量/月 |
| 静态页面托管 | 免费 | 免费(在配额内) | 不额外收费 |
| Serverless 函数 | 免费 100GB-小时/月 | 付费后前 1000GB-小时含在月费中 | 轻量 API 消耗约 5GB-小时/月 |
| Edge Functions | 免费 100万次 | 付费后前 2000 万次含在月费中 | 中等 SaaS 约 50-200 万次/月 |
| 数据传输 | 免费 1GB/月 | 1TB 内免费,超出 $0.15/GB | 国内用户传图/视频要注意 |
| Web Analytics | 免费 2500 事件 | 付费后免费 10 万件 | 需开启计费才可用 |
Postgres 账单(以 Neon 为例)
| 项目 | Free Tier | Pro | 说明 |
|---|---|---|---|
| 存储 | 500MB | 10GB+ | SaaS 起步够用 |
| 连接数 | 无限制(Serverless 驱动) | 更多 | 冷启动友好 |
| 计算时间 | 免费 190 计算小时 | $0.024/计算小时 | 轻量 API 月耗 <50 计算小时 |
| 月费 | $0 | ~$19 起 | 免费带子域名 |
示例:一个 10 人团队、日活千人的 Next.js SaaS
- Vercel Pro(2 用户):$40/月
- Postgres(Neon Pro,550 计算小时):~$13/月
- 数据传输 80GB(在 1TB 内):$0
- 总计约 $53/月
如果你流量很大(比如视频/图片 CDN),数据传输会迅速拉高账单。此时应把静态资源托管到 R2/S3 + Cloudflare,避免 Vercel 带宽费用。详见 Vercel 定价与成本详解。
9. 从 Demo 到”像样的 SaaS”的下一步
在上面的骨架上,你可以逐步加东西:
用户系统 & 登录
- 加
auth:NextAuth.js / Lucia / 自写 JWT / Clerk 等; - 在
UserWorkspace中控制用户对 workspace 的访问与权限。
- 加
更完整的多租户模型
路由策略:
- Path-based:
/app/[workspaceSlug]/... - Domain-based:
[workspaceSlug].your-saas.com(可利用 Vercel 的多域名 + 中间件解析 Host);
- Path-based:
每个请求先解析当前 workspace,然后把 workspace 信息注入到请求上下文。
计费 & 订阅
- Stripe / Paddle 等;
- 在
Workspace中增加plan,billingStatus,seats等字段。
观测与监控
- 开启 Vercel Analytics;
- 或接入 Sentry、Datadog 等。
常见问题(FAQ)
Vercel 部署 Next.js 需要写 Dockerfile 吗?
不需要。Vercel 原生识别 Next.js 项目,自动选择安装命令(npm install)、构建命令(npm run build)和输出目录,连 Git 仓库后 push 即部署。只有当你部署非 Next.js 的自定义后端(Go、Java 等)时才需要容器,那更适合 Render / Railway。
Prisma 能跑在 Vercel Edge Runtime 吗?
不能直接使用。Prisma 依赖 Node.js 原生绑定,Route Handler 和页面默认的 Node.js runtime 可以正常工作;Edge Runtime(export const runtime = "edge")下需要使用 Prisma Accelerate / Data Proxy 这类 HTTP 访问层。简单原则:数据库访问代码留在 Node runtime,Edge 只做中间件和轻量逻辑。
Neon、Supabase、Vercel Postgres 怎么选?
三者都是托管 Postgres,对 Next.js + Prisma 的接入方式完全一致(一个 DATABASE_URL)。Neon 的 Serverless 驱动对冷启动最友好、免费额度慷慨;Supabase 附带 Auth/Storage/Realtime 全家桶;Vercel Postgres(底层也是 Neon)集成最省事,在 Dashboard 一键创建并自动注入环境变量。Demo 阶段任选其一即可,迁移成本只是换连接串。
多租户数据隔离只靠 workspace_id 过滤安全吗?
本文 Demo 级别的做法(每个查询带 where: { workspaceId })适合原型,但生产环境建议加两道保险:在 API 入口统一解析并校验当前用户对 workspace 的权限(结合 UserWorkspace 角色),以及使用 Postgres 的行级安全(RLS)做数据库层兜底。更完整的多租户模型见 JAMstack SaaS 多租户架构模型。
部署后 Prisma migrate 怎么执行?
不要在 Vercel 构建命令里跑 migrate dev(它只用于本地开发)。生产迁移的标准做法是在本地或 CI 中对生产数据库执行 npx prisma migrate deploy,再触发 Vercel 部署;也可以把 prisma migrate deploy && next build 作为构建命令,但要确保构建环境能连通数据库。
相关阅读
- Vercel 详解:前端与 AI 应用的一站式云平台
- Vercel 与 Netlify、Render、Railway、Cloudflare Pages 对比
- Vercel 定价与成本详解
- Vercel 国内访问优化指南
- Vercel Edge Functions 深度指南
- Vercel 部署故障排查
- JAMstack 架构深度解析
- JAMstack SaaS 多租户架构模型
- Vercel 专题导航
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。