Novu多租户如何用Contexts隔离不同组织的通知
2026/9/10 9:56:21 网站建设 项目流程

Novu多租户如何用Contexts隔离不同组织的通知

【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu

如果你的产品是 SaaS 或多组织(multi-org)形态,同一个用户可能同时属于 Acme、Globex 等多个组织,而你在 Novu 中又只有一个项目、一套 workflow。直接给每个组织复制一套 workflow、或者用组织名前缀区分 subscriberId,会让维护成本快速失控。Novu 提供的做法是:用 Contexts 中的tenant上下文作为组织边界,在触发 workflow 时带上租户 context,再在<Inbox />组件上传入相同的 context,让每个用户只看到自己所在组织的通知。

这条路径来自官方文档 Multi-tenancy、Inbox with Context 与 Manage contexts,前提是你已经在 Novu 中有一个可触发的工作流和可用的 secret key,前端使用@novu/react渲染<Inbox />

先明确两个限制,避免后面踩坑

在动手之前,文档给出了两条硬性约束,直接影响 tenant context 的写法:

  • 一次 workflow trigger 最多传5 个 context 键
  • 每个 context 的data对象序列化后不能超过64KB

tenant context 每个键支持两种写法,二选一:

// 简单写法:只有租户 ID context: { tenant: 'acme-corp' } // 富对象写法:ID + 可用于模板渲染的元数据 context: { tenant: { id: 'acme-corp', data: { name: 'Acme Corporation', logo: 'https://cdn.acme.com/logo.png' } } }

data里的公司名、logo、套餐类型等元数据用于模板个性化;它不参与Inbox 的过滤匹配——匹配只按 context 的 type 和 id 进行。这一点决定了后面所有验证方式,值得记住。

第 1 步:定义 tenant context(可跳过)

Novu 会自动创建 context,所以这一步严格来说不是必须:第一次在 trigger 或<Inbox />中引用某个tenant:<id>时,Novu 会自动创建(just-in-time)。但如果你希望元数据有一份明确的初始来源,可以在两个地方预创建:

方式 A:Novu dashboard。登录 Novu dashboard,在侧边栏点Contexts,新建时填写三个字段(见 Manage contexts):

  • Identifier:该类型下的唯一标识,例如acme-corp
  • Context type:类别,这里填tenant
  • Custom data (JSON):可选,如{ "name": "Acme Corporation", "plan": "enterprise" }

Create context保存。

方式 B:API。用 cURL 直接创建,<NOVU_SECRET_KEY>替换为你环境中的 secret key:

curl -L -g -X POST 'https://api.novu.co/v2/contexts' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: ApiKey <NOVU_SECRET_KEY>' \ -d '{ "type": "tenant", "id": "acme-corp", "data": { "name": "Acme Corporation", "plan": "enterprise" } }'

注意:如果相同的type:id组合(这里是tenant:acme-corp)已存在,API 创建请求会失败,这可以作为“该租户已注册”的判断依据。

元数据更新规则(来自 Manage contexts):

  • workflow trigger 中带data的富对象会整体替换存储的data(不是合并),行为类似 subscriber upsert;
  • trigger 中只传字符串 id 时,复用已有 context,不修改data
  • <Inbox />等面向 subscriber 的入口只会查找或创建 context,永不更新已有data,不要依赖 Inbox 来刷新租户元数据;
  • context 的typeid不可变,只有data可更新,且更新时整个data被替换。

第 2 步:触发 workflow 时带上 tenant context

workflow 触发时 Novu 会先检查该 context 是否已存在:不存在就自动创建,存在且你传了data就更新,只传字符串 id 则原样复用。

Node.js SDK 示例(<YOUR_SECRET_KEY_HERE>workflowIduser-123均替换为你自己的值,workflowId是 Novu 中目标 workflow 的 ID):

import { Novu } from "@novu/api" const novu = new Novu({ secretKey: "<YOUR_SECRET_KEY_HERE>" }); await novu.trigger({ workflowId: "workflowId", to: { subscriberId: "user-123" }, payload: { amount: "$250", plan: "Pro", }, context: { tenant: { id: "acme-corp", data: { name: "Acme Corporation", plan: "enterprise", }, }, }, });

不想装 SDK 时可以用 cURL 打 trigger 接口,效果一致(name字段即 workflow 标识,<NOVU_SECRET_KEY>换成你的 secret key):

curl -L -g -X POST 'https://api.novu.co/v1/events/trigger' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: ApiKey <NOVU_SECRET_KEY>' \ -d '{ "name": "workflowId", "to": { "subscriberId": "user-123" }, "payload": { "amount": "$250", "plan": "Pro" }, "context": { "tenant": { "id": "acme-corp", "data": { "name": "Acme Corporation", "plan": "enterprise" } } } }'

多组织应用里,同一个 subscriber(如user-123)可以分别以tenant: acme-corptenant: globex触发同一个 workflow,得到的就是两条属于不同租户的通知,不需要为每个组织复制 workflow。

第 3 步:给<Inbox />传入相同的 tenant context

隔离的另一半在展示端。把触发时使用的 tenant context 传给<Inbox />contextprop(APPLICATION_IDENTIFIERSUBSCRIBER_ID替换为你应用中的应用标识和 subscriber):

import { Inbox } from '@novu/react'; <Inbox applicationIdentifier="APPLICATION_IDENTIFIER" subscriber="SUBSCRIBER_ID" context={{ tenant: { id: 'acme-corp', data: { name: 'Acme Corporation', plan: 'enterprise', }, }, }} />

过滤按type + id 精确匹配,Inbox 中每个 context 条目解析成tenant:acme-corp这样的键,只显示触发时使用了相同键集合的通知。官方文档给出的匹配组合如下:

Workflow ContextInbox Context是否显示
{ "tenant": "acme" }{ "tenant": "acme" }
{ "tenant": { "id": "acme", "data": { "name": "Acme" } } }{ "tenant": { "id": "acme" } }
{ "tenant": "acme" }{ "tenant": { "id": "acme" } }
{}{ "tenant": "acme" }
{ "tenant": "acme" }{ "tenant": "globex" }
{ "tenant": "acme" }{}
{ "tenant": "acme", "app": "first" }{ "tenant": "acme" }

从上表可以推出几条实用结论:

  • trigger 带 context、Inbox 不带(或带了不同的 context)时,通知不会出现在该 Inbox 会话中;反过来 trigger 不带而 Inbox 带 context,同样不显示。
  • 嵌套data不需要两边一致,例如 Inbox 只传{ tenant: { id: "acme" } }就能匹配带data的 trigger。
  • 键集合必须完全一致:trigger 用了tenant+app两个键,Inbox 只传tenant就匹配不上。
  • 用户在应用内切换组织时,用新的 context 重新渲染<Inbox />,Novu 会自动重新拉取通知并切换 WebSocket 订阅。

第 4 步:验证隔离是否生效

文档提供了三个可以互相印证的检查点:

  1. Dashboard 的 Contexts 区。自动创建和手动创建的 context 都会出现在侧边栏Contexts列表中,确认tenant: acme-corp存在且data符合预期。
  2. Activity Feed 按 context 检索运行记录(见 Applying context):进入Activity FeedWorkflow Runs标签,在搜索栏点Context,以type:id格式(如tenant:acme-corp)搜索,应能找到该租户相关的所有执行。
  3. API Traces 查看解析后的 context。从 Activity Feed 的Requests列表选择对应运行,进入API Traces标签,可以看到 Novu 实际收到并解析的完整 context 对象,用于确认data是否按预期写入。

前端侧的验证就是行为本身:Acme 的 Inbox 会话只显示以tenant: acme-corp触发的通知,其他租户的通知被自动排除。

通知“发送成功”但 Inbox 里看不到时怎么排查

这是官方 FAQ 明确列出的典型现象(Inbox with Context):in-app 任务在 activity feed 中状态为 "Success",但通知不出现在 Inbox 中。原因是 Inbox 按 context 的 type/id 过滤,任务成功只代表消息写入了对应 context 的作用域,不代表当前 Inbox 会话能看到它。官方给出的检查顺序:

  1. trigger 带了 context,<Inbox />就必须带上相同 type/id的 context;
  2. trigger 不带 context,就不要<Inbox />contextprop;
  3. 用户切换租户后,确认已用新 context 重新渲染<Inbox />

逐项对照上面的匹配表,基本都能定位是哪一种不匹配。

可选增强:按租户定制通知内容

context 的data会注入到所有模板编辑器(in-app、email、SMS、push)中,通过{{context}}访问器读取,例如:

<p>Welcome, new user from {{context.tenant.data.name}}!</p> <p>Your account is on the {{context.tenant.data.plan}} plan.</p>

也可以在步骤的Step conditions里用 context 做条件分支,比如仅在context.tenant.data.planenterprise时发送企业版专属邮件。这样一套 workflow 定义就能服务所有租户,不必为每个租户复制模板。详见 Applying context。

生产环境:用 contextHash 防止客户端篡改 context

contextprop 在客户端设置,恶意用户可以修改它去窥探其他租户的通知。生产环境必须从你的服务端获取 context 详情与contextHash,一并传给<Inbox />。开启 HMAC 后:subscriberHash始终必需;只要给<Inbox />传了contextcontextHash也必需(不传context则只需要subscriberHash)。

计算方式(对传给组件的同一个context 对象做规范化后再 HMAC-SHA256,NOVU_SECRET_KEY为你的 secret key):

import { createHmac } from 'crypto'; import { canonicalize } from '@tufjs/canonical-json'; const context = { tenant: { id: "acme-corp", data: { name: "Acme Corporation", plan: "enterprise", }, }, }; const contextHash = createHmac('sha256', "NOVU_SECRET_KEY") .update(canonicalize(context)) .digest('hex');

contextHash与通知匹配是两回事:它校验的是你传给<Inbox />的精确 context 对象(含data字段),所以要 hash 的正是组件收到的那个对象。组件侧再把它与subscriberHash一起传入:

<Inbox applicationIdentifier="YOUR_APPLICATION_IDENTIFIER" subscriber="YOUR_SUBSCRIBER_ID" subscriberHash={subscriberHash} context={context} contextHash={contextHash} />

HMAC 的完整配置见 Prepare for Production。

小结与边界

  • 隔离的两侧必须对齐:trigger 与<Inbox />使用相同 type/id 的 tenant context,键集合一致、data可以不一致;
  • context 自动创建、trigger 带data时整体替换元数据、Inbox 永不更新data,元数据的更新要走 API/dashboard 或服务端 trigger;
  • 单次 trigger 最多 5 个 context 键,单个data上限 64KB;
  • 删除 context 不可恢复,清理前确认没有仍依赖它的 workflow(Manage contexts)。

后续如果需要按 context 管理更多租户元数据或做 provider 级路由,从 Contexts API 和 Manage contexts 继续深入即可。

【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询