用户接入自定义域名时,通常只关心一个结果:app.example.com 能不能打开。
产品界面也很容易把这件事做成一个输入框:填 hostname,复制 CNAME,等待生效。这个体验应该简单,但系统内部不能只把它当成字符串保存下来。一个自定义域名开始接流量之前,Appaloft 需要确认几件事:这个 hostname 属于谁,谁授权了 DNS 改动,目标 route 是什么,证书和网关是否准备好,失败时应该停在哪里。
这篇接着一个部署 URL,应该说清楚什么会变往下讲。上一篇说 URL 本身也有承诺:immutable deployment URL、mutable alias、production alias、custom domain 和 blocked route 不能混在一个 url 字段里。今天只看 custom domain 这一路。
从 hostname 开始
流程的第一步很普通:用户在某个 project、environment 或 resource 里输入一个 hostname。
比如:
pocketbase.example.com
这时系统还不能立刻创建公开 route。它最多可以创建一个 pending domain binding,并开始做几类检查:
- hostname 格式是否有效;
- 是否落在平台禁止或保留的后缀里;
- 当前组织、项目或资源是否有权申请这个 binding;
- 同一个 hostname 或 path 是否已经被其他 active resource 使用;
- 目标 deployment、resource 或 static artifact 是否有可路由的 target;
- 需要用户走自动 DNS 授权,还是展示 manual DNS records。
这些检查看起来琐碎,但它们决定了后面能不能 fail closed。如果两个 resource 同时争用同一个 hostname,默认结果应该是停下来,而不是抢占。如果用户输入的是 Appaloft 官方默认域名后缀,系统也不应该把它当成普通 customer domain。
Appaloft 的 routing docs 里把这类 URL 分开命名:official default subdomain、immutable preview/deployment URL、mutable alias、production alias、custom domain binding。名字多一点,但后续状态会清楚很多。
DNS 授权有两条路
Appaloft 当前为 Cloudflare DNS 设计的自助路径是 Domain Connect。
用户点击 “Connect Cloudflare DNS” 后,Appaloft 生成一份确定性的 DNS plan,浏览器跳到 Cloudflare 的 Domain Connect 授权流程。用户在 provider 页面确认,Cloudflare 应用 template,Appaloft 再回来做 DNS readback。
这个流程里有一个重要边界:Domain Connect 是一次用户授权的 DNS setup flow。Appaloft 不需要为了这个场景长期保存用户的 Cloudflare token。授权完成也不等于 Appaloft 盲目信任浏览器 redirect;最后仍然要通过 DNS readback 证明记录真的出现了。
另一条路是 manual DNS。
如果 provider 不支持 Domain Connect,用户取消授权,或者当前域名只能手动配置,Appaloft 应该展示一组确定性的 DNS records:要添加什么 record,record type 是什么,host/name 怎么填,target 指向哪里,TXT verification 需要什么值。用户改完 DNS 后,再回到 Appaloft 刷新 readback。
这条路慢一些,但很重要。自定义域名不能依赖单一 provider,也不能因为自动授权不可用就鼓励用户复制不透明命令。manual DNS 是 fallback,也是诊断材料。
平台凭证和用户域名要分开
Appaloft 可以拥有自己的 official domain provider。例如 Cloud 的默认访问域名可以由平台控制,走 Appaloft 自己的 wildcard DNS、TLS 和 gateway。
这不代表平台凭证可以替用户管理 example.com。
用户的 custom domain 要靠用户侧授权或 DNS ownership proof。Appaloft runtime 里的 Cloudflare token、Worker route、官方后缀配置,只能证明平台能管理 Appaloft-owned zone。它们不能证明某个 tenant 有权把 pocketbase.example.com 指向自己的 resource。
这条边界在 AI 场景里更明显。Agent 可以帮用户发起 “把这个 resource 绑定到这个域名” 的意图,也可以调用 CLI/API 查询 DNS readiness。它不应该拿到长期 provider credential,更不应该绕过产品的 domain binding flow 直接改 DNS。
所以 Appaloft 的 Connections 模型把事情拆开:
ConnectorDefinition描述一种 provider 能力,例如 Cloudflare DNS;Connection记录某个 owner 已经授权的连接实例;ConnectionCapability描述这个 connection 能执行的动作,例如dns.records.apply;CredentialGrant只在对应生命周期里处理授权材料;- DNS 页面、CLI alias 和 AI tools 都委托给同一套 connection capability。
用户看到的是一个域名流程,系统内部看到的是 owner-scoped capability。
先生成 DNS plan
当用户准备应用 DNS 改动时,Appaloft 应该先给出 plan。
DNS plan 至少要说清楚:
- 要创建或验证哪些 records;
- record 指向哪个 route target;
- 是否需要 TXT ownership verification;
- 是否会覆盖已有 record;
- 是否发现冲突;
- 哪个 connection 或 manual path 会执行;
- apply 之后需要怎样 readback;
- 失败时用户能看到什么诊断。
这个 plan 不应该包含 token、provider 原始响应、内部 gateway secret 或任何像 secret 的值。对于 automatic path,plan 可以告诉用户会跳转到 provider 授权。对于 manual path,plan 可以展示可复制的 DNS records。
如果 plan 发现已有 record 与 Appaloft 请求冲突,默认应该停下。覆盖用户自己的 DNS record 是高风险动作,不能藏在“一键接入”的 happy path 里。
这和 Blueprint install plan 的原则很像:先让用户看到平台准备做什么,再进入 accepted apply。
Readback 决定能不能继续
DNS apply 之后,最容易写错的地方是把 “provider API 返回成功” 当成完成。
Appaloft 更关心 readback。
Domain Connect 完成后,要重新查 DNS。Manual DNS 配好后,也要重新查 DNS。DNS 传播可能需要时间,CAA 或证书签发可能有独立问题,CDN 或 gateway route 也可能还没准备好。产品应该把这些状态拆开:
- DNS record missing;
- DNS record present but target mismatch;
- ownership TXT missing;
- conflicting active route;
- TLS pending;
- route target unavailable;
- HTTP reachability pending;
- ready。
这比一句 “waiting for DNS” 细一些,但对用户和支持团队都更有用。用户知道自己是在等传播,还是填错了 target。支持团队能看到 DNS、TLS、route、HTTP readback 卡在哪一层。AI agent 也不会因为某个 shallow check 通过就宣布完成。
路由生效是另一个阶段
DNS ready 后,还要把 domain binding 接到 route target。
一个理想的 route activation flow 大概是:
create route intent
-> verify ownership or official suffix authority
-> create DNS/CDN/gateway route
-> provision/attach TLS
-> activate route target
-> verify HTTP reachability
-> publish route read model
Cloud 当前的生产 routing 还有一些外部系统 gap:真实 DNS/TLS/CDN/ACME/readback 必须有 go-live evidence,不能在文档或文章里写成已经全部自动化完成。已有的规则是:route model 和 readiness 要先把边界表达清楚,真实 provider mutation 只在批准的生产路径里发生。
这也是为什么 Appaloft 不把 domain binding 写成一个简单 boolean。verified: true 不够。更有用的是状态和证据:谁验证了,验证了哪个 hostname,target 是什么,证书状态如何,route readback 是否通过,是否有冲突,是否处于 blocked/disabled。
失败时要安全
自定义域名流程里,失败是常态。DNS 记录还没传播,用户填了 apex 和 subdomain 混合配置,已有记录冲突,证书没签下来,route target 被删除,或者 provider 授权窗口过期。
失败时 Appaloft 应该保留三个原则。
第一,失败不要创建半激活 route。没有 ownership proof,就不要接流量。
第二,诊断要具体,但不能泄露旧 target、内部 host、provider account secret、raw token 或 request body。用户需要知道该改哪条 DNS record,不需要看到平台内部的 gateway 细节。
第三,cleanup 要保守。用户删除 binding 后,平台可以清理自己创建或标记过的资源,但不能按宽泛条件删除用户手工创建的 DNS 数据。能自动清理就显示清理结果,不能自动清理就给出明确的 manual cleanup gap。
这和部署里的 blocked route 规则是一组问题。一个不可用的 domain 也应该安全地不可用。
CLI 和 AI 看到同一条路径
自定义域名经常会出现在 Web Console 之外。
用户可能在 CLI 里跑:
appaloft dns connect example.com --provider cloudflare
appaloft dns plan example.com --hostname pocketbase.example.com --target ...
appaloft domain-binding dns-readiness ...
AI agent 也可能从 MCP tool 或 operation catalog 调用同样的能力。
这些入口不应该各自发明一条逻辑。Web 的 “Connect Cloudflare DNS” 按钮、CLI 的 appaloft dns connect、domain binding readiness、future MCP tool,都应该回到同一套 connector/category/capability、authz、audit、plan、accept、apply 和 readback。
这样做的好处很实际:如果 Web 上会检查冲突,CLI 也会检查;如果 Web 不会打印 secret,agent tool 也不会拿到 secret;如果一个 DNS apply 需要 owner/admin 权限,自动化入口也不能跳过去。
Agent 可以帮助用户走流程,但流程本身仍然属于产品。
用户最后只需要一个清楚状态
完整流程听起来长:
- 输入 hostname。
- 创建 pending binding。
- 检查冲突和权限。
- 生成 DNS plan。
- 选择 Domain Connect 或 manual DNS。
- 应用或等待用户配置 records。
- 做 DNS ownership/readback。
- 准备 TLS 和 gateway route。
- 激活 route target。
- 做 HTTP reachability readback。
- 发布 ready 状态和诊断。
产品不需要把每一步都变成用户负担。好的界面可以只显示当前要做的动作:连接 DNS、复制 records、刷新检查、等待证书、完成。
但控制面需要保存这些细节。否则出了问题,只会剩下一句 “domain not ready”。这句话无法告诉用户下一步该做什么,也无法让 agent、CLI、workflow 或支持团队继续接手。
Appaloft 想要的自定义域名接入,是一个可以被人和工具共同理解的流程:先确认所有权,再计划 DNS 改动,再 readback,再激活 route。最后用户得到的当然还是一个能打开的 URL。区别在于,这个 URL 背后有证据,有边界,也知道自己什么时候不该接流量。