一、适用场景
适用于这类 Web 项目:
- Next.js 自部署
- 前面有 nginx 反向代理
- 同时存在主站域名和用户子域名
- 主站负责登录、后台、API
- 子域名负责公开展示页或分享页
典型例子:
jianli.live:首页、登录、控制台、APIalice.jianli.live:Alice 的简历分享页
二、先说结论
长期最好维护的方案,不是把所有页面都塞进同一套域名逻辑里,而是明确分层:
- 主站 host 只承载应用能力
- 用户子域名只承载公开内容
- 内部回源地址只给 nginx 和服务端使用
- 域名配置只保留一套真相源
如果这四件事混在一起,后期一定会出现这些问题:
- 环境变量越配越多,且含义重叠
- 本地、预发、生产行为不一致
- 子域名上也能访问登录页和后台页
- 分享逻辑和后台预览逻辑耦合
- Auth cookie、回调地址、Host 头问题越来越难排查
三、推荐的职责拆分
1. APP_PUBLIC_ORIGIN
主站规范入口。
例子:
APP_PUBLIC_ORIGIN=https://jianli.live
用途:
- canonical 基准地址
- 主站页面跳转目标
- 主站绝对 URL 构建
- 登录后回跳和对外文档说明
2. APP_ROOT_DOMAIN
根域名,只包含 hostname。
例子:
APP_ROOT_DOMAIN=jianli.live
用途:
- 子域名拼接
- Host 分类
- 保留子域名识别
3. APP_INTERNAL_ORIGIN
Next.js 实际监听地址,仅用于内部回源。
例子:
APP_INTERNAL_ORIGIN=http://127.0.0.1:3000
用途:
- nginx
proxy_pass - 服务端内部调用
- SSR / RSC 下的内部 API 基地址
4. AUTH_TRUST_HOST
反向代理场景必须开启。
AUTH_TRUST_HOST=true
用途:
- 让 Auth.js 信任代理转发过来的 Host / Proto
四、最佳实践架构
1. 主站和子域名必须职责分离
推荐约定:
- 主站 host:
//auth/*/dashboard/*/api/*
- 用户子域名 host:
/- 未来的公开内容路径
不推荐:
alice.example.com/auth/loginalice.example.com/dashboardalice.example.com/api/*
原因:
- 会把后台能力暴露到任意租户 host
- 增加 Auth、缓存、SEO、日志分析复杂度
- 让”主站”和”租户站点”边界模糊
补充:主站公开路径也要跳回主站
不仅是 /auth、/dashboard、/api 这类后台路径,像 /privacy、/terms 这类主站公开页,也不应该在租户 host 上承载。访问时应跳回主站:
https://alice.jianli.live/terms → 302 → https://jianli.live/terms
补充:APP_PUBLIC_ORIGIN 为子域名时的根域请求
如果 APP_PUBLIC_ORIGIN 本身是根域名的子域名(如 APP_PUBLIC_ORIGIN=https://app.jianli.live),那么裸根域名请求(jianli.live)应该跳回主站,而不是作为”root host”处理。
2. 公开分享和后台预览必须分离
推荐:
- 后台预览页:主站内受保护路由
- 对外分享页:子域名 + 访问码
不推荐:
- 后台预览直接复用分享链接
- 登录态用户通过子域名看“owner 视图”
原因:
- 预览是产品后台能力
- 分享是对外访问能力
- 两者权限模型、SEO、审计需求完全不同
五、推荐的请求流
1. 主站访问
浏览器访问:
https://jianli.live/dashboard
流程:
- nginx 收到请求
- 保留
Host和X-Forwarded-* - 转发到
127.0.0.1:3000 - Next.js 识别为主站 host
- 正常渲染后台页面
2. 子域名访问
浏览器访问:
https://alice.jianli.live/?code=abc123
流程:
- nginx 收到请求
- 转发原始
Host - Next.js Proxy 识别
alice是租户子域名 - 内部 rewrite 到租户内部路由
- 页面读取分享码并查询公开内容
3. 子域名误访问后台
浏览器访问:
https://alice.jianli.live/dashboard
推荐处理:
- 直接 302/307 重定向到主站:
https://jianli.live/dashboard
而不是继续在租户 host 上渲染。
六、Next.js 中的推荐实现方式
1. 域名配置统一放服务端
推荐建立一个集中模块,例如:
src / config / domain.ts;
职责:
- 校验
APP_PUBLIC_ORIGIN - 校验
APP_ROOT_DOMAIN - 推导
publicHost - 推导
internalOrigin - 推导是否启用
trustHost
不要把这些逻辑分散在:
next.config.jsmiddleware/proxy- Auth 配置
- Client Component
- README
更不要每个地方各写一份校验。
2. 客户端不要直接依赖 NEXT_PUBLIC_* 域名变量
这是一个非常容易踩的坑。
原因:
NEXT_PUBLIC_*会在 build 阶段内联进 bundle- 一旦需要“一套镜像部署多个环境”,就会失控
- staging / preview / custom domain 会更难维护
推荐做法:
- 域名配置只在服务端读取
- 如果客户端只需要显示根域名,用 provider 或 props 下发
3. Proxy 只负责 Host 分类和 rewrite / redirect
推荐让 Proxy 只做这几件事:
- 判断当前 host 是主站、租户、保留域名还是异常域名
- 主站请求直接放行
- 租户访问后台路径时跳回主站
- 租户访问公开路径时 rewrite 到内部租户路由
不推荐让 Proxy 参与:
- 权限判断
- 业务数据查询
- 登录态分支渲染
4. 内部租户路由要单独命名,且必须禁止直达
推荐:
/tenant/[subdomain]
安全约束(两层防护):
-
Proxy 层拦截:禁止直接访问内部租户路由。任何直达
/tenant/*的请求都会被重定向到主站首页(proxy.ts 拦截)。 -
页面层校验:内部租户页面必须校验请求来自正常的 proxy rewrite。页面读取
x-tenant-rewrite和x-tenant-subdomain请求头,只有两者匹配才正常渲染,否则 404。
// 页面侧校验示例
if (requestHeaders.get("x-tenant-rewrite") !== "1") {
notFound();
}
if (requestHeaders.get("x-tenant-subdomain") !== subdomain) {
notFound();
}
这样即使有人绕过 proxy 直接请求内部路由,页面层也会拒绝渲染。
不推荐直接 rewrite 到:
/[username]
原因:
- 路由意图更清晰
- 以后容易扩展更多公开路径
- 不会把“路径动态路由”和“host 租户路由”混为一谈
七、Auth.js 最佳实践
1. 反向代理场景开启 AUTH_TRUST_HOST
这是必须项。
AUTH_TRUST_HOST=true
2. 尽量不要覆盖默认 session cookie 策略
除非你非常确定需要自定义,否则优先使用 Auth.js 默认 cookie 行为。
不推荐一开始就手写:
- cookie name
- cookie secure
- cookie domain
- cookie sameSite
原因:
- 本地 HTTP 和生产 HTTPS 行为不同
- 代理层、子域名、主域名组合复杂时很容易出边界问题
- 后期迁移到别的域名策略会更痛苦
3. 如果子域名不承载后台,就没必要强依赖跨子域登录态
这是一个非常重要的简化点。
如果你的产品策略是:
- 后台只在主站
- 子域名只做公开展示
那么 Auth 就只需要把主站登录做好,不需要为了“子域名 owner 视图”强行拉高复杂度。
八、SEO 与分享卡片最佳实践
1. 根布局设置 metadataBase
在根布局设置:
metadataBase
用于统一生成 canonical、Open Graph、相对 metadata URL。
2. 租户公开页生成自己的 metadata
租户页应该根据实际子域名生成:
titledescriptionopenGraph.urlrobots
3. 分享码不要进入 metadata URL
不推荐把:
?code=abc123
写进 metadata 或 canonical。
原因:
- 容易泄漏访问码
- 会污染抓取和缓存
- 分享码本质上是访问凭证,不是页面身份
九、nginx 最佳实践
推荐配置:
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:3000;
关键点:
- 必须传原始
Host - 必须传
X-Forwarded-Proto - 本地开发也尽量走 nginx,不要直接访问
localhost:3000
这样本地与线上行为才一致。
十、本地开发最佳实践
推荐本地也模拟真实访问方式:
APP_PUBLIC_ORIGIN=http://jianli.localhost
APP_ROOT_DOMAIN=jianli.localhost
APP_INTERNAL_ORIGIN=http://127.0.0.1:3000
AUTH_TRUST_HOST=true
推荐访问方式:
http://jianli.localhosthttp://alice.jianli.localhost
不推荐:
http://localhost:3000
原因:
localhost:3000会绕过真实 Host 逻辑- 很多子域名问题在本地根本测不出来
十一、推荐的代码组织方式
src/
├── app/
│ ├── [username]/ # 兼容跳转到子域名
│ ├── tenant/[subdomain]/[[...slug]] # 内部租户公开页
│ ├── auth/ # 登录注册
│ ├── dashboard/ # 后台页面
│ └── api/ # API 路由
├── components/
│ ├── providers/PublicDomainProvider.tsx
│ └── dashboard/
├── config/
│ └── domain.ts # 域名真相源
├── lib/
│ ├── subdomain.ts # Host 分类和 URL 拼接
│ └── subdomain.schemas.ts # 子域名校验 schema(所有子域名校验共用此文件)
├── proxy.ts # Host 路由分发
└── server/
├── auth/
└── api/
十二、推荐的设计原则
原则 1:域名配置只保留一套真相源,子域名校验也必须共用一份 schema
域名配置不要同时存在:
- 一套给 Next config
- 一套给 Auth
- 一套给客户端
- 一套写在 README
子域名校验也必须共用同一份 schema(src/lib/subdomain.schemas.ts),禁止在多个路由或 API 里重复定义校验逻辑。所有注册、检查、更新接口必须引用同一份 schema,确保校验规则始终一致。
原则 2:Host 路由和业务权限分层
- Proxy 管 Host
- 页面和 API 管权限
原则 3:主站能力和租户展示能力分离
- 主站是应用
- 子域名是内容展示
原则 4:本地开发尽量模拟生产链路
- 同样的 Host
- 同样的代理头
- 同样的域名规则
十三、反面示例
以下做法不推荐:
1. 用 NEXT_PUBLIC_* 作为域名核心配置
问题:
- build 时写死
- 多环境难复用
2. 子域名继续承载 /auth 和 /dashboard
问题:
- 主站边界模糊
- 后续权限和日志治理困难
3. 后台预览直接复用分享链接
问题:
- 预览和分享权限耦合
- 产品语义不清晰
4. 在多个文件里重复校验域名合法性
问题:
- 一处改规则,多处忘同步
5. 本地只用 localhost:3000
问题:
- 子域名、Host、Auth 问题几乎无法提前发现
十四、上线前检查清单
APP_PUBLIC_ORIGIN是否配置正确APP_ROOT_DOMAIN是否只包含 hostnameAPP_INTERNAL_ORIGIN是否指向 Next.js 实际监听地址AUTH_TRUST_HOST=true是否开启- nginx 是否透传
Host和X-Forwarded-* - 子域名访问
/dashboard是否会跳回主站 - 子域名公开页是否只通过分享码可访问
- 后台预览是否只在主站受保护路由可见
- 根布局是否设置
metadataBase - 租户页 metadata 是否不泄漏分享码
十五、一句话总结
多域名、多子域名项目的核心,不是“把所有页面都支持任意 host 访问”,而是:
把主站、租户展示、内部回源、Auth、分享链路的职责边界切干净。
边界一旦清晰,配置会少很多,问题会少很多,后期加自定义域名也会顺很多。