关于 multi-tenant-architecture
这是一项用于在 Cloudflare 或 Vercel 上设计多租户平台的开发技能,指导租户识别、路由、计算与数据隔离、域名及平台限制等决策,并提供平台参考资料和验证检查。
主要功能
- 01选择 Cloudflare 或 Vercel 架构
- 02规划租户域名与识别方式
- 03设计计算和数据隔离
- 04制定路由及租户上下文传递
- 05处理自定义域名与平台限制
- 06用证据命令验证边界
适用场景
- 01规划多租户 SaaS 平台
- 02设计租户路由与隔离
- 03准备自定义域名接入流程
- 04检查租户边界实现
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mblode/ agent-skills --skill multi-tenant-architecture安装后在 Claude Code 中输入/multi-tenant-architecture确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/cloudflare-platform.mddata-isolation.mdlimits-and-quotas.mdpsl.mdvercel-domains.mdvercel-platform.md详细介绍
多租户平台架构(Cloudflare 或 Vercel)
- **涵盖:**平台选择、域名策略和 PSL、租户识别、计算与数据隔离、主机名路由、租户上下文传播、自定义域名和 SSL、每租户静态文件,以及将平台限制映射到套餐。
- **不涵盖:**通用文件夹结构或模块契约(使用
codebase-architecture)、搭建新仓库(使用scaffold-nextjs),或在路由动态提供服务后每租户 SEO 文件的内容:站点地图条目、规范 URL、结构化数据、索引策略(使用seo)。
目录
- 平台分流(先决定)
- 参考文件
- 工作流(顺序很重要)
- 注意事项
- 输出架构
- 证据命令
- 相关技能
平台分流(先决定)
| 特征 | 平台 | 模型 |
|---|---|---|
| 租户上传或生成自己的代码;代码级隔离;在 KV、D1、Durable Objects、R2 上进行边缘计算 | Cloudflare | 在每租户 Worker 的分发命名空间前放置分发 Worker;自定义主机名使用 Cloudflare for SaaS |
| 每个租户运行相同的 Next.js 代码库,仅在内容、品牌和套餐上有所不同;使用 ISR、Server Components、Vercel 部署 | Vercel | 单一部署;proxy.ts 根据主机名解析租户;项目上配置通配符域名和自定义域名 |
- 每个产品只选择一个平台。用 Cloudflare 代理置于 Vercel 应用前,会叠加 TLS 和重定向层,通常会导致重定向循环和证书签发失败。
- 在 Vercel 上让租户部署自己的代码属于多项目模型(每个租户一个 Vercel 项目,通过 SDK 创建)。其隔离逻辑与 Cloudflare 行相同;本技能中的 Vercel 参考仅涵盖单一部署模型。
参考文件
| 文件 | 阅读时机 |
|---|---|
| cloudflare-platform.md | 选择 Cloudflare 时:分发命名空间、路由、Cloudflare for SaaS 自定义主机名、隔离模式、KV 和 D1(步骤 3 到 7) |
| vercel-platform.md | 选择 Vercel 时:proxy.ts 解析、App Router 布局、Global Config 查询、每租户静态文件、自定义子路径、本地开发(步骤 4 到 7) |
| vercel-domains.md | 选择 Vercel 时:SDK 域名生命周期、DNS 目标、验证、通配符名称服务器、SSL、故障排除(步骤 7) |
| data-isolation.md | 任一平台的步骤 3:共享架构搭配 RLS、每租户一个架构、每租户一个数据库,以及 Postgres/Supabase/Drizzle 策略模式 |
| psl.md | 步骤 1:当租户在同级子域名上发布内容或运行代码时,了解资格要求、提交方式和过渡期 Cookie 控制 |
| limits-and-quotas.md | 步骤 8:Cloudflare、Vercel 和 Neon 的限制名称及各自官方来源,用于映射到套餐 |
工作流(顺序很重要)
复制此清单以跟踪进度:
Multi-tenant progress:
- [ ] Step 1: Domain strategy and PSL decision
- [ ] Step 2: Tenant identification strategy
- [ ] Step 3: Isolation model (compute and data)
- [ ] Step 4: Deterministic routing
- [ ] Step 5: Tenant context propagation
- [ ] Step 6: Tenant config and least-privilege bindings
- [ ] Step 7: Custom domains and per-tenant static files
- [ ] Step 8: Limits mapped to plans, evidence captured
- 选择域名策略
- 将租户工作负载放在专用的可注册域名上(租户使用
acme.app,品牌使用acme.com)。一个进行钓鱼活动的租户若位于x.acme.com,就可能导致整个域名被列入屏蔽列表;而设置了Domain=acme.com的租户 Cookie 则可以访问你的控制台。 - 将控制台和身份验证放在与租户子域名不同的顶级域名下(控制台使用
app.acme.com,租户使用*.acme.app)。 - 如果租户在同级子域名上发布内容或运行代码,请将租户名称正上方的标签提交到 PSL(
acme.app,或者<tenant>.sites.acme.app对应的sites.acme.app),并立即开始:没有服务级别协议。租户自有的自定义域名无需 PSL 条目。否则,记录No PSL并说明原因。
- 选择租户标识方式(一个主要方式;自定义域名作为升级路径)
- 子域名
tenant.acme.app:通配符 DNS 加通配符证书。默认选项。 - 自定义域名
tenant.com:租户将 CNAME 指向你。适用于付费租户;声誉归属转移给他们;需要完成第 7 步中的入驻生命周期。 - 路径
acme.app/tenant:无需为每个租户配置 DNS 或证书,但无法隔离 Cookie,也没有品牌标识。仅当租户永远不会获得主机名时才选择此方式。
- 定义隔离模型
- **计算,Cloudflare:**在非可信模式下使用单个 dispatch 命名空间;按计划限制每次调用的
cpuMs和subRequests;如果租户代码可能访问互联网,则使用出站 Worker。 - **计算,Vercel:**使用单个部署,绝不执行租户代码。如果租户必须发布代码,请改用 Vercel 多项目或 Cloudflare,而不要在应用内进行沙箱隔离。
- **数据:**默认采用共享架构,每个面向租户的表都包含
tenant_id,并启用 RLS;对于受监管或资源占用较大的租户,可按计划选择每租户一个数据库。参见 data-isolation.md。
- 确定性路由(租户绝不影响路由,也无法看到彼此的数据)
- **Cloudflare:**在 SaaS 区域上设置单个
*/*路由,指向 dispatch Worker;主机名 -> 租户记录(KV,未命中时查询 D1)->env.DISPATCHER.get(script);Worker not found-> 404。 - Vercel:
proxy.ts(Next.js 16;在 15 中使用runtime: 'nodejs'的middleware.ts)读取host,在 Global Config 或数据库中查找租户,并重写到租户路径段;未知主机名 -> 404,绝不指向品牌站点。 - 在任何租户重写之前放行
/.well-known。将robots.txt、sitemap.xml和llms.txt路由到租户路径段,使其内容因租户而异。
- 从单一权威来源传递租户上下文
- 删除所有传入的
x-tenant-*标头,根据已解析的租户设置x-tenant-id、x-tenant-slug、x-tenant-plan,并在请求中转发这些标头(NextResponse.next({ request: { headers } }))。服务器组件读取await headers();路由处理程序读取request.headers。Cloudflare:dispatch Worker 设置标头,或在fetch前传递参数。 - 代理负责路由,而非授权。Server Functions、路由处理程序和作业都必须从会话重新解析租户,并由数据层强制执行租户隔离(RLS 或
tenant_id谓词)。
- 仅绑定租户所需的内容
- **Cloudflare:**每个用户 Worker 都有自己的绑定(KV 命名空间、D1 数据库、R2 前缀);添加绑定必须显式重新部署。不要使用共享全局变量。
- **Vercel:**Global Config 仅存储
hostname -> { id, slug, plan };数据库是事实来源,域名验证通过后执行写穿。功能标志和品牌信息从以租户 ID 为键的数据库中读取。
- 支持自定义域名和每租户静态文件
- 需要设计并记录的生命周期:添加域名 -> 显示 DNS 目标 -> 验证所有权 -> 颁发证书 -> 激活映射 -> 删除或故障处理路径。
- **Cloudflare:**在 SaaS 区域上使用 Cloudflare for SaaS 自定义主机名、代理回退源、
customers.<you>.comCNAME 目标、http或txt验证,并在 DNS 切换前预先验证。参见 cloudflare-platform.md。 - Vercel:
projectsAddProjectDomain-> 项目域名卡片中的 DNS 值 -> 仅当域名已在 Vercel 上时添加_vercelTXT ->projectsVerifyProjectDomain-> Let's Encrypt HTTP-01。参见 vercel-domains.md。 robots.txt、sitemap.xml、llms.txt是租户路径段中的路由处理程序,需明确指定Content-Type;/public中不得存放任何租户专属内容。这些内容属于seo范畴。
- 将限制呈现为计划并收集证据
- 根据 limits-and-quotas.md 填写限制与计划对照表,从各自的来源 URL 读取当前值并注明日期;在路由层实施限制(Cloudflare
limits、Vercel 计划标头加服务端检查)。 - 不要在请求路径中执行任何长时间运行的操作:使用 Cloudflare Queues 或 Workflows、Vercel 后台函数或 cron。
- 每项租户操作(创建租户、添加域名、验证、移除)都能通过 HTTP 完成,且权限与 UI 相同;如果某项操作只能在控制面板中进行,平台逻辑就渗入了 UI。
- 运行证据命令并将结果粘贴到输出中。
注意事项
与平台和数据相关的注意事项位于相应步骤的参考资料中;这些问题会在加载任何参考资料前触发。
- 转发传入的租户标头:执行
curl -H "x-tenant-id: <other>"后会提供另一个租户的数据。在代理或分发 Worker 的每条路径上(包括跳过解析的路径,如/.well-known和品牌站点),都要删除或覆盖x-tenant-*。 - 如果主机名映射以受限字符集为键(Global Config 仅允许
^[\w-]+$),则需要无冲突的编码方式或哈希;将点替换为下划线会使a-b.example.com和a.b-example.com映射到同一个键。在记录中保留原始主机名,并在读取时进行比对。 - 如果一开始采用基于路径的方案,同时将自定义域名列入后续计划,那么之后就需要进行 URL 重写、Cookie 修改和 DNS 迁移。
输出架构
内容长度取决于决策:删除项目不会涉及的章节,不要为了填充而保留。
# Multi-tenant architecture
## Platform decision
- Platform: Cloudflare | Vercel
- Why this platform:
- Rejected platform and reason:
## Domain map
- Brand domain:
- Tenant domain:
- Tenant subdomains:
- Custom domains:
- PSL decision: Submit (suffix, owner, PR link, _psl TXT date) | No PSL (reason)
## Routing matrix
| Host pattern | Resolver | Destination | Unknown tenant behavior |
|---|---|---|---|
## Tenant context flow
- Authority: proxy.ts | dispatch Worker
- Headers set and stripped:
- Server read path:
- Data-layer enforcement:
## Isolation model
- Compute isolation:
- Data isolation (and per-plan variant):
- Config/binding isolation:
## Custom-domain lifecycle
1. DNS target:
2. Ownership verification:
3. Certificate provisioning:
4. Routing activation:
5. Removal/failure path:
## Limits-to-plan table
| Limit | Source URL / access date | Free | Pro | Enterprise | Enforcement point |
|---|---|---:|---:|---:|---|
## Validation evidence
| Check | Command | Expected | Result |
|---|---|---|---|
证据命令
在确定设计完成前,针对本地环境或预览环境运行;如标记为 N/A,请说明原因。
| 检查项 | 命令 | 预期结果 |
|---|---|---|
| 代码中存在租户边界 | rg -n "x-tenant-id|CREATE POLICY|FORCE ROW LEVEL SECURITY|DISPATCHER.get" . | 在代理或分发 Worker 以及架构中均有匹配项 |
| 未知主机返回 404 | curl -sI -H "Host: nope.acme.app" <url> | 404 |
| 忽略伪造的标头 | curl -s -H "Host: a.acme.app" -H "x-tenant-id: tenant-b" <url>/api/whoami | 租户 A |
| 静态文件内容有所不同 | curl -s -H "Host: a.acme.app" <url>/robots.txt 与 -H "Host: b.acme.app" | 内容不同,Content-Type: text/plain |
| 应用角色的 RLS 有效 | psql -c "BEGIN; SET LOCAL ROLE app_user; SELECT set_config('app.tenant_id','<t1>',true); SELECT count(*) FROM posts; ROLLBACK;" | 仅包含租户 t1 的行 |
| ACME 路径可访问 | curl -sI -H "Host: tenant.com" <url>/.well-known/acme-challenge/test | 不会重定向到租户路径段 |
| 限制信息为最新 | 在限制表的各个 URL 旁注明访问日期 | 日期位于规划周期内 |
相关技能
codebase-architecture:应用本身的文件夹结构、模块契约和请求上下文流水线。scaffold-nextjs:先引导 Next.js turborepo,再应用这些多租户模式。seo:路由开始提供相应内容后,各租户robots.txt、sitemap.xml、llms.txt的内容、规范 URL 和结构化数据。
仅用于维护:evals/evals.json 包含此技能变更的回归场景;用户任务期间不会加载该文件。
评论
支持 Markdown