Vercel 部署 Next.js + Postgres 全栈实战:从零上线多租户 SaaS

手把手用 Vercel 部署 Next.js + Postgres 多租户 SaaS 应用:Prisma 数据建模、workspace 租户隔离、Route Handler API、环境变量配置与 Node.js 运行时注意事项,附完整代码与 curl 验证命令。

本文演示如何在 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,这里用一个通用思路:

  1. 去 Neon / Supabase / Railway / Vercel Postgres 创建一个 Postgres 实例。
  2. 拿到一个标准的连接串,形如:
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 下创建一个 project
  • GET: 获取该 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 上创建项目

  1. 打开 Vercel Dashboard,点击 New Project

  2. 选择刚刚的 Git 仓库 saas-demo

  3. Vercel 会自动识别这是 Next.js App Router 项目,构建命令一般为:

    • Install command: npm install
    • Build command: npm run build
    • Output dir: .next

可以保持默认。

7.3 配置环境变量

在 Vercel 项目设置的 Environment Variables 里配置:

  • DATABASE_URL = 刚才的 Postgres 连接串

建议:

  • ProductionPreview 环境都配置同样的变量,或者至少 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 TierPro说明
存储500MB10GB+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”的下一步

在上面的骨架上,你可以逐步加东西:

  1. 用户系统 & 登录

    • auth:NextAuth.js / Lucia / 自写 JWT / Clerk 等;
    • UserWorkspace 中控制用户对 workspace 的访问与权限。
  2. 更完整的多租户模型

    • 路由策略:

      • Path-based:/app/[workspaceSlug]/...
      • Domain-based:[workspaceSlug].your-saas.com(可利用 Vercel 的多域名 + 中间件解析 Host);
    • 每个请求先解析当前 workspace,然后把 workspace 信息注入到请求上下文。

  3. 计费 & 订阅

    • Stripe / Paddle 等;
    • Workspace 中增加 plan, billingStatus, seats 等字段。
  4. 观测与监控

    • 开启 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 作为构建命令,但要确保构建环境能连通数据库。

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「saas」更多文章