用 Vercel 部署一个 Next.js + Postgres 的 SaaS Demo
By Leeting Yan
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
就能看到刚才添加的项目列表。
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 等。