One agent mailbox per tenant in a multi-tenant SaaS

One agent mailbox per tenant in a multi-tenant SaaS

多租户 SaaS 中的每个租户一个代理邮箱

Most multi-tenant SaaS apps that send email do it from one shared identity. There’s a notifications@yourapp.com, every customer’s mail flows through it, and the tenant is just a from_name you stamp on the subject line or a footer you swap out. That’s fine until it isn’t — until Tenant A’s spam complaints drag down Tenant B’s deliverability, until a reply from a customer lands in a single firehose inbox you now have to fan back out, until one tenant wants a stricter send cap than another and you realize you built none of that into the data model. 大多数发送电子邮件的多租户 SaaS 应用都使用同一个共享身份。比如 notifications@yourapp.com,所有客户的邮件都通过它发送,而租户仅仅是你印在主题行上的发件人名称或替换掉的页脚。这在出问题之前看起来没问题——直到租户 A 的垃圾邮件投诉拖累了租户 B 的送达率,直到客户的回复涌入一个你需要手动分发的单一“消防栓”式收件箱,直到某个租户想要比其他租户更严格的发送上限,而你意识到你的数据模型中根本没有构建这些功能。

So let’s not share. Let’s give every tenant its own real mailbox — a dedicated Agent Account per customer, each with its own grant_id, its own send identity, its own policy and limits, grouped into its own workspace. Not one inbox with a thousand label hacks. A thousand inboxes, isolated by construction. 所以,我们不要共享了。让我们为每个租户提供一个真实的独立邮箱——为每个客户提供一个专用的代理账户(Agent Account),每个账户都有自己的 grant_id、发送身份、策略和限制,并归入各自的工作区。不要再用一个收件箱加一千个标签这种“黑客”手段了。我们要的是一千个在架构上就相互隔离的收件箱。

I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for when I’m wiring this up. Every step gets the two-angle tour: the raw curl call and the nylas command that does the same thing. 我负责 Nylas CLI 的开发,所以下面这些终端命令是我在进行相关配置时实际使用的。每一步我都会提供两种方式:原始的 curl 调用和实现相同功能的 nylas 命令。

Why per-tenant beats one shared sender

为什么“每个租户一个发送者”优于“共享发送者”

The shared-sender model fails along a few predictable seams. Per-tenant Agent Accounts close each one: 共享发送者模型在几个可预见的环节上会失效。而每个租户一个代理账户可以解决这些问题:

  • Deliverability blast radius. When everyone sends from one address, one tenant’s bounce rate and spam complaints poison the reputation everyone shares. Per-tenant accounts — and, if you want, per-tenant domains — keep one customer’s bad behavior from sinking the rest. 送达率影响范围。 当所有人都从同一个地址发送邮件时,一个租户的退信率和垃圾邮件投诉会损害大家共享的信誉。每个租户一个账户(如果需要,还可以每个租户一个域名)可以防止一个客户的不良行为拖累其他人。

  • Inbound that actually belongs to someone. A shared sender means replies come back to one mailbox and you’re left correlating them to tenants by hand. When each tenant has its own grant, an inbound message.created event already carries the grant_id. The routing is done before your handler runs. 真正归属于个人的入站邮件。 共享发送者意味着回复会回到同一个邮箱,你必须手动将它们与租户关联。当每个租户拥有自己的授权(grant)时,入站的 message.created 事件已经携带了 grant_id。路由在你的处理程序运行之前就已经完成了。

  • Per-tenant policy and limits. Different customers, different rules. A trial tenant capped at a low daily send; an enterprise tenant with a higher quota and longer retention. With a shared sender you’d build all of that yourself. Here it’s a policy attached to a workspace. 租户级别的策略和限制。 不同的客户有不同的规则。试用租户的每日发送量较低;企业租户则有更高的配额和更长的保留期。使用共享发送者,你需要自己构建所有这些功能。而在这里,它只是附加到工作区的一个策略。

  • Clean offboarding. When a tenant churns, you delete their grant. Their mail, their send identity, their limits — gone in one call, with nothing tangled into a shared mailbox you can’t safely prune. 干净的离网流程。 当租户流失时,你只需删除他们的授权。他们的邮件、发送身份、限制——一次调用即可全部清除,不会有任何东西纠缠在无法安全清理的共享邮箱中。

The grant is the whole abstraction

授权(Grant)是核心抽象

Here’s the part worth sitting with: an Agent Account is just a Nylas grant with a grant_id. There’s nothing new to learn on the data plane. Every grant-scoped endpoint you already know — Messages, Drafts, Threads, Folders, Attachments, Contacts, Calendars, Events, Webhooks — works against a tenant’s Agent Account exactly the way it works against a Gmail or Microsoft grant you got through OAuth. The provider is nylas instead of google, and that’s the only difference your code sees. 这一点值得深思:代理账户本质上就是一个带有 grant_id 的 Nylas 授权。在数据层面没有什么新东西需要学习。你已经熟悉的每一个授权范围内的端点——消息、草稿、会话、文件夹、附件、联系人、日历、事件、Webhooks——在租户的代理账户上运行的方式,与你通过 OAuth 获取的 Gmail 或 Microsoft 授权完全一样。提供商是 nylas 而不是 google,这就是你的代码所能看到的唯一区别。

So “one mailbox per tenant” doesn’t mean a new integration per tenant. It means N grants, all driven by the same code paths, distinguished only by which grant_id you put in the URL. Your tenant table grows one column — the grant ID — and everything else you already built keeps working. 因此,“每个租户一个邮箱”并不意味着每个租户都需要一个新的集成。它意味着 N 个授权,全部由相同的代码路径驱动,唯一的区别是你放入 URL 中的 grant_id。你的租户表只需增加一列——grant ID——而你已经构建的其他所有内容都能继续工作。

The isolation lives one level up, in two objects: 隔离发生在更高一层,通过两个对象实现:

  1. A workspace groups grants (usually by email domain) and carries one policy_id and an array of rule_ids. Every grant in the workspace inherits both. 工作区(Workspace) 将授权分组(通常按电子邮件域名),并携带一个 policy_id 和一组 rule_id。工作区中的每个授权都会继承这两者。

  2. A policy bundles the limits (send quota, storage, retention) and spam settings that apply to every account in any workspace pointing at it. 策略(Policy) 捆绑了适用于指向该策略的任何工作区中每个账户的限制(发送配额、存储空间、保留期)和垃圾邮件设置。

So the architecture is: one grant per tenant, grouped into workspaces, governed by policies. 因此,架构是:每个租户一个授权,分组到工作区中,并由策略进行管理。

Let’s build it.

让我们开始构建。

Before you begin 开始之前

  • An API key. All requests authenticate with Authorization: Bearer <NYLAS_API_KEY>, and the key identifies your application. Examples here hit https://api.us.nylas.com. API 密钥。 所有请求都使用 Authorization: Bearer <NYLAS_API_KEY> 进行身份验证,该密钥用于标识你的应用程序。此处的示例请求地址为 https://api.us.nylas.com

  • At least one verified domain. Agent Accounts live on a domain — a custom one you register and publish DNS records for, or a Nylas trial subdomain like yourapp.nylas.email. New domains warm up over roughly four weeks, so register production domains early. The DNS walkthrough is in the provisioning docs. If you’ve run nylas init, the CLI already points at your application. 至少一个已验证的域名。 代理账户存在于一个域名上——你可以注册并发布 DNS 记录的自定义域名,或者像 yourapp.nylas.email 这样的 Nylas 试用子域名。新域名大约需要四周的预热期,所以请尽早注册生产环境域名。DNS 设置指南在配置文档中。如果你已经运行了 nylas init,CLI 已经指向了你的应用程序。

A note on the domain decision, because it drives your tenant-isolation design. You have two shapes: 关于域名决策的一点说明,因为它决定了你的租户隔离设计。你有两种模式:

  • One shared domain, one address per tenant. Every tenant gets tenant-<id>@agents.yourapp.com. Simplest to operate. Tenants still get isolated grants, inboxes, and policies — they just share a sending domain and therefore share domain-level reputation. 一个共享域名,每个租户一个地址。 每个租户获得 tenant-<id>@agents.yourapp.com。操作最简单。租户仍然拥有隔离的授权、收件箱和策略——只是共享一个发送域名,因此共享域名级别的信誉。

  • One domain per tenant. acme.youragents.com, globex.youragents.com. Maximum isolation: a tenant’s deliverability reputation is entirely their own. More DNS to manage, and each new domain warms separately. Reach for this when a tenant brings their own domain, or when reputation isolation is a selling point. 每个租户一个域名。 acme.youragents.com, globex.youragents.com。最大程度的隔离:租户的送达率信誉完全属于他们自己。需要管理更多的 DNS,且每个新域名都需要单独预热。当租户自带域名,或者信誉隔离是一个卖点时,请选择此方案。

A single Nylas application can manage Agent Accounts across any number of registered domains, so you can mix both — shared domain for the long tail, dedicated domains for the accounts that need them. 单个 Nylas 应用程序可以管理跨任意数量已注册域名的代理账户,因此你可以混合使用——长尾客户使用共享域名,需要特殊对待的账户使用专用域名。

Provision a mailbox for a tenant

为租户配置邮箱

Each tenant’s account is one POST /v3/connect/custom with "provider": "nylas" and the address in settings.email. The optional top-level name becomes the default From display name on everything that account sends — so each tenant’s outbound mail can carry that tenant’s own brand. 每个租户的账户通过一次 POST /v3/connect/custom 请求创建,设置 "provider": "nylas" 并在 settings.email 中填入地址。可选的顶层 name 字段将成为该账户发送的所有邮件的默认发件人显示名称——这样每个租户的外发邮件都可以带有他们自己的品牌。

curl --request POST \
  --url "https://api.us.nylas.com/v3/connect/custom" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "provider": "nylas",
    "name": "Acme Corp",
    "settings": {
      "email": "acme@agents.yourapp.com"
    }
  }'

The response hands back data.id. Store it on the tenant row — that’s the grant_id you’ll use on every subsequent call for this customer. This mapping (tenant → grant) is the spine of the whole design. 响应会返回 data.id。将其存储在租户行中——这就是你后续针对该客户进行每次调用时要使用的 grant_id。这种映射(租户 → 授权)是整个设计的核心。

From the CLI it’s one line: 使用 CLI 只需一行命令:

nylas agent account create acme@agents.yourapp.com --name "Acme Corp"

That provisions the grant and prints its id, status, and connector details. If the underlying nylas connector doesn’t exist on your application yet, the CLI creates it first. 这会配置授权并打印其 ID、状态和连接器详细信息。如果你的应用程序中尚不存在底层的 nylas 连接器,CLI 会先创建它。