关于 novu-framework-integration
面向使用 `@novu/framework` 在应用代码中定义 Novu 通知工作流的开发者。技能介绍类型化触发负载、渠道与操作步骤,以及供 Novu Cloud 获取工作流内容和数据的 Bridge Endpoint。
主要功能
- 01在应用代码中定义工作流
- 02校验并类型化触发负载
- 03构建渠道与操作步骤
- 04为可编辑步骤设置 Schema
- 05通过 Bridge Endpoint 暴露工作流
适用场景
- 01将通知工作流纳入源代码管理
- 02在执行期间获取最新数据
- 03允许团队成员在 Dashboard 编辑步骤内容
- 04组合延迟、摘要和 HTTP 请求
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ novuhq/ skills --skill novu-framework-integration安装后在 Claude Code 中输入/novu-framework-integration确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/bridge-endpoint.mddeployment.mdemail-templates.mdexamples.mdschema-validation.mdsecurity.mdstudio-and-cli.mdtranslations.mdworkflow-and-steps.md详细介绍
框架集成
使用 @novu/framework 通过代码构建通知工作流,并与应用源代码一同管理。工作流保存在你的代码仓库中,内容使用你现有的库(React Email、Vue Email、Svelte Email)进行渲染;单个 HTTP 端点(Bridge)即可让 Novu Cloud 使用来自你服务的即时数据执行工作流。
通过代码构建工作流时使用此技能。对于在 Novu Dashboard 中创建的工作流,只需通过
trigger-notification触发即可——无需使用 Framework。
何时使用 Framework
| 使用 Framework | 使用 Dashboard 工作流 |
|---|---|
| 工作流必须纳入源代码管理 / GitOps | 所有内容都由非技术同事负责 |
| 需要从数据库 / API 获取即时数据 | 所有数据都能放入触发载荷 |
| 使用 React/Vue/Svelte Email 渲染邮件 | 区块编辑器已足够 |
| 执行自定义代码(LLM、第三方 API) | 纯发送流程 |
| 需要类型化载荷和步骤控制 | 快速原型 |
两种方式可以共存——同一环境中可以同时使用代码定义的工作流和 Dashboard 定义的工作流。
工作原理
- 使用
@novu/framework中的workflow(...)在代码中定义工作流。 - 在应用中公开单个
/api/novuHTTP 路由——即 Bridge 端点。 - 将 Bridge URL 同步到 Novu Cloud(通过
npx novu sync或 GitHub Action)。 - 在工作流执行期间,Novu Cloud 会通过经过身份验证的隧道调用 Bridge,以获取包含最新数据的步骤内容。
Trigger ──► Novu Cloud Worker ──► Your Bridge (/api/novu) ──► Provider (SendGrid, FCM, …)
快速开始
1. 初始化项目
npx novu init --secret-key=<YOUR_NOVU_SECRET_KEY>
这会创建一个示例 Bridge 应用,其中包含工作流、环境文件和可正常工作的 /api/novu 路由。
2. 或添加到现有应用
npm install @novu/framework zod @react-email/components react-email
NOVU_SECRET_KEY=<YOUR_NOVU_SECRET_KEY>
3. 定义工作流
import { workflow } from "@novu/framework";
import { z } from "zod";
export const welcomeWorkflow = workflow(
"welcome-email",
async ({ step, payload, subscriber }) => {
await step.email("send-email", async (controls) => {
return {
subject: controls.subject,
body: `Welcome ${subscriber.firstName ?? payload.userName}!`,
};
}, {
controlSchema: z.object({
subject: z.string().default("Welcome to {{payload.appName}}"),
}),
});
},
{
payloadSchema: z.object({
userName: z.string(),
appName: z.string().default("Acme"),
}),
name: "Welcome Email",
description: "Sent when a new user signs up",
tags: ["onboarding"],
}
);
4. 挂载 Bridge 端点
选择与你的框架匹配的封装方式——请参阅下方的 Bridge 端点设置。
5. 运行本地 Studio
npx novu@latest dev --port <YOUR_APP_PORT>
打开 http://localhost:2022,即可预览工作流、编辑控件并触发测试事件。Studio 会自动创建一个公共隧道,以便 Novu Cloud 访问你的本地 Bridge。
工作流结构
正在设计工作流本身?请参阅
design-workflow/,了解渠道选择、严重程度、critical、摘要默认值、步骤条件和 9 个参考模板(订单确认、付款失败、账户暂停、评论、试用期即将结束、密码重置、Webhook 扇出、先获取再通知)。Framework SKILL 介绍的是如何在代码中表达这些决策;design-workflow/介绍的是需要做出哪些决策。
workflow(workflowId, handler, options);
| 参数 | 类型 | 说明 |
|---|---|---|
workflowId | string | 环境中的唯一标识符 |
handler | ({ step, payload, subscriber }) => Promise<void> | 工作流主体——按顺序调用各个步骤 |
options | WorkflowOptions | 架构、名称、描述、标签和偏好设置 |
工作流选项
| 选项 | 类型 | 用途 |
|---|---|---|
payloadSchema | ZodSchema | JsonSchema | ClassValidatorClass | 验证触发载荷,并推断 payload 的 TS 类型 |
name | string | 显示在 Dashboard / <Inbox /> 中的易读名称 |
description | string | 显示在 Dashboard 中的描述 |
tags | string[] | 用于分类筛选 / Inbox 标签页 |
severity | 'low' | 'medium' | 'high' | 在 Inbox 中进行视觉优先级排序。大多数工作流无需设置。 |
critical | boolean | 绕过订阅者偏好设置、跳过摘要且不等待延迟。仅用于必须送达的事件。 |
preferences | WorkflowPreferences | 默认渠道偏好设置和 readOnly 标记 |
工作流上下文
处理器接收 { step, payload, subscriber }:
step— 渠道和操作步骤构建器(step.email、step.delay、step.digest,等等)payload— 在触发时传入的强类型数据,并根据payloadSchema进行验证subscriber— 收件人的{ subscriberId, firstName?, lastName?, locale?, data?, ... }
渠道步骤
所有渠道步骤的结构相同:
await step.<channel>(stepId, resolver, options?);
| 步骤 | 必需输出 | 显著输出 | 返回结果 |
|---|---|---|---|
step.email | subject、body | attachments、from、replyTo | 否 |
step.sms | body | — | 否 |
step.push | title(或 subject)、body | data、image、icon | 否 |
step.chat | body | —(按提供商覆盖) | 否 |
step.inApp | body | subject、avatar、redirect、primaryAction、secondaryAction、data | { seen, read, lastSeenDate, lastReadDate } |
Email 步骤
await step.email("welcome", async (controls) => ({
subject: controls.subject,
body: render(<WelcomeEmail name={subscriber.firstName} />),
from: "[email protected]",
replyTo: "[email protected]",
}));
In-App 步骤(富载荷)
await step.inApp("inbox", async () => ({
subject: "Welcome to Acme!",
body: "We are excited to have you on board.",
avatar: "https://acme.com/avatar.png",
redirect: { url: "/welcome", target: "_self" },
primaryAction: {
label: "Get Started",
redirect: { url: "/get-started", target: "_self" },
},
data: { entityType: "user", entityId: payload.userId },
}));
In-App 步骤会返回 { seen, read, lastSeenDate, lastReadDate } — 使用该结果来决定后续步骤的 skip。
SMS / Push / Chat
await step.sms("verification", async () => ({
body: `Your code is ${payload.code}`,
}));
await step.push("new-message", async () => ({
title: "New message",
body: payload.preview,
data: { messageId: payload.id },
}));
await step.chat("notify", async () => ({
body: `:rocket: Deploy ${payload.id} succeeded`,
}));
操作步骤
step.delay
在执行下一步骤前暂停工作流。
await step.delay("wait-a-day", async () => ({
unit: "days",
amount: 1,
}));
支持的 unit 值:seconds、minutes、hours、days、weeks、months。
step.digest
在一段时间内将多个触发事件聚合为一条通知。
const { events } = await step.digest("daily", async () => ({
unit: "days",
amount: 1,
digestKey: payload.projectId, // optional — group by custom key
}));
await step.email("summary", async () => ({
subject: `${events.length} updates today`,
body: render(<DigestEmail events={events} />),
}));
对于基于 cron 的摘要,请使用 cron: "0 0 * * *",而非 unit/amount。每个摘要事件包含 { id, time, payload }。每个工作流只能有一个摘要 — 如果需要两阶段摘要,请通过 step.custom 链接第二个工作流。
step.http
在工作流中调用外部 HTTP 端点 — 用于 webhook 扇出,或即时获取数据。
const plan = await step.http("fetch-plan", async () => ({
method: "GET",
url: `https://api.example.com/users/${payload.userId}/plan`,
responseBodySchema: {
type: "object",
properties: { planName: { type: "string" }, renewalDate: { type: "string" } },
required: ["planName", "renewalDate"],
} as const,
}));
await step.email("notify", async () => ({
subject: `Your ${plan.planName} plan`,
body: `Renews on ${plan.renewalDate}.`,
}));
Webhook 风格:
await step.http("webhook", async () => ({
method: "POST",
url: payload.webhookUrl,
headers: [{ key: "Content-Type", value: "application/json" }],
body: [
{ key: "event", value: "payment_failed" },
{ key: "subscriberId", value: subscriber.subscriberId },
],
continueOnFailure: true,
}));
后续步骤引用 HTTP 响应数据时,HTTP 步骤必须声明 responseBodySchema。只有在架构中声明的属性才能通过 {{ steps.<http-step-id>.<property> }} 访问。
step.custom
运行任意代码,并保存其结果以供后续步骤使用。
const task = await step.custom("fetch-task", async () => {
const t = await db.fetchTask(payload.taskId);
return { id: t.id, title: t.title, complete: t.complete };
}, {
outputSchema: {
type: "object",
properties: {
id: { type: "string" },
title: { type: "string" },
complete: { type: "boolean" },
},
required: ["id", "complete"],
} as const,
});
await step.email("reminder", async () => ({
subject: `Reminder: ${task.title}`,
body: "Please complete your task.",
}), {
skip: () => task.complete,
});
自定义步骤的结果只能在后续步骤的 resolver、providers 和 skip 函数中使用,不能用于步骤控件。
步骤选项
await step.email(stepId, resolver, {
controlSchema, // Zod | JSON Schema | Class-Validator class
skip, // (controls) => boolean | Promise<boolean>
providers, // per-provider override callbacks
disableOutputSanitization, // boolean — for raw HTML in Inbox
});
skip
有条件地跳过步骤。接收已解析的控件。
await step.email("follow-up", resolver, {
skip: () => inAppNotification.read === true,
});
providers(按步骤覆盖提供商配置)
自定义发送给底层提供商的请求 — 例如 Slack blocks 或 SendGrid cc。
await step.email("alert", resolver, {
providers: {
sendgrid: ({ controls, outputs }) => ({
from: "[email protected]",
cc: ["[email protected]"],
_passthrough: {
body: { ip_pool_name: "transactional" },
headers: { "X-Custom": "value" },
},
}),
},
});
_passthrough 会深度合并到最终的提供商请求中 — 类型化的提供商键优先于 _passthrough。
disableOutputSanitization
允许输出中包含原始 HTML / 未转义字符(例如 In-App data.link 中的 &):
await step.inApp("link", async () => ({
body: "Check it out",
data: { link: "/p/123?active=true&env=prod" },
}), { disableOutputSanitization: true });
载荷架构
载荷是在触发时传入的数据。定义架构即可获得类型化的 payload 并进行运行时验证。
使用 Zod
import { z } from "zod";
workflow("comment", handler, {
payloadSchema: z.object({
postId: z.number(),
authorName: z.string(),
comment: z.string().max(200),
}),
});
使用 JSON Schema
workflow("comment", handler, {
payloadSchema: {
type: "object",
properties: {
postId: { type: "number" },
authorName: { type: "string" },
comment: { type: "string", maxLength: 200 },
},
required: ["postId", "comment"],
additionalProperties: false,
} as const,
});
TypeScript 需要
as const才能从 JSON Schema 推断载荷类型。
使用 Class Validator
import { IsString, IsNumber } from "class-validator";
class CommentPayload {
@IsNumber() postId!: number;
@IsString() authorName!: string;
@IsString() comment!: string;
}
workflow("comment", handler, { payloadSchema: CommentPayload });
需要 class-validator、class-validator-jsonschema、reflect-metadata。请参阅 references/schema-validation.md。
步骤控件 — 团队成员无需编写代码即可使用
控件是步骤级输入,非技术同事可以在 Novu Dashboard UI 中编辑,而无需修改代码。控件会通过你定义的架构进行验证(Zod / JSON Schema / Class-Validator)。
await step.email("welcome", async (controls) => ({
subject: controls.subject,
body: render(<EmailTemplate hideBanner={controls.hideBanner} />),
}), {
controlSchema: z.object({
hideBanner: z.boolean().default(false),
subject: z.string().default("Hi {{subscriber.firstName | capitalize}}"),
}),
});
控件中的变量
控件值支持 LiquidJS 模板:
{{subscriber.firstName}}— 任意订阅者属性{{payload.userId}}—payloadSchema中定义的任意载荷字段{{payload.invoiceDate | date: '%a, %b %d, %y'}}— Liquid 过滤器{{subscriber.firstName | append: ': ' | append: payload.status | capitalize}}— 链式过滤器
在 Dashboard UI 中输入 {{,即可自动补全可用变量。
控制项与负载
| 控制项 | 负载 | |
|---|---|---|
| 编辑者 | 仪表板中的非技术同事 | 代码中的开发者 |
| 架构 | 每个步骤的 controlSchema | 每个工作流的 payloadSchema |
| 持久化 | 按环境存储在 Novu Cloud 中 | 在触发时发送 |
| 用例 | 主题、文案、样式、行为开关 | 每次触发时的动态数据 |
工作流偏好设置
在代码中定义默认渠道偏好设置。有关完整的偏好设置解析模型,请参阅 manage-preferences。
workflow("system-alert", handler, {
preferences: {
all: { enabled: true, readOnly: false },
channels: {
email: { enabled: true },
sms: { enabled: false },
inApp: { enabled: true },
},
},
});
all.readOnly: true会将工作流设为关键工作流——订阅者无法禁用它。- 对于
channels中未指定的任何渠道,all.enabled是回退值。 - 省略时的默认值:所有渠道均为
enabled: true、readOnly: false。
Bridge 端点设置
Bridge 是一个 HTTP 路由(默认为 /api/novu),Novu Cloud 会通过它调用你的应用,以便:
- 发现已注册的工作流(
GET) - 为指定的订阅者和负载解析步骤内容(
POST) - 验证请求中的 HMAC 签名
每个框架都提供了一个 serve 包装器,用于处理解析、HMAC 验证和响应格式化。
Next.js(App Router)
import { serve } from "@novu/framework/next";
import { welcomeWorkflow } from "@/novu/workflows";
export const { GET, POST, OPTIONS } = serve({
workflows: [welcomeWorkflow],
});
Next.js(Pages Router)
import { serve } from "@novu/framework/next";
import { welcomeWorkflow } from "../../novu/workflows";
export default serve({ workflows: [welcomeWorkflow] });
Express
import express from "express";
import { serve } from "@novu/framework/express";
import { welcomeWorkflow } from "./novu/workflows";
const app = express();
app.use(express.json()); // required
app.use("/api/novu", serve({ workflows: [welcomeWorkflow] }));
app.listen(4000);
NestJS
import { Module } from "@nestjs/common";
import { NovuModule } from "@novu/framework/nest";
import { welcomeWorkflow } from "./novu/workflows";
@Module({
imports: [
NovuModule.register({
apiPath: "/api/novu",
workflows: [welcomeWorkflow],
}),
],
})
export class AppModule {}
如需依赖注入,请使用 NovuModule.registerAsync——参阅 references/bridge-endpoint.md。
Remix
import { serve } from "@novu/framework/remix";
import { welcomeWorkflow } from "../novu/workflows";
const handler = serve({ workflows: [welcomeWorkflow] });
export { handler as action, handler as loader };
SvelteKit
import { serve } from "@novu/framework/sveltekit";
import { welcomeWorkflow } from "$lib/novu/workflows";
export const { GET, POST, OPTIONS } = serve({ workflows: [welcomeWorkflow] });
Nuxt
import { serve } from "@novu/framework/nuxt";
import { welcomeWorkflow } from "~/novu/workflows";
export default defineEventHandler(serve({ workflows: [welcomeWorkflow] }));
H3
import { createApp, eventHandler, toNodeListener } from "h3";
import { createServer } from "node:http";
import { serve } from "@novu/framework/h3";
import { welcomeWorkflow } from "./novu/workflows";
const app = createApp();
app.use("/api/novu", eventHandler(serve({ workflows: [welcomeWorkflow] })));
createServer(toNodeListener(app)).listen(4000);
AWS Lambda
import { serve } from "@novu/framework/lambda";
import { welcomeWorkflow } from "./novu/workflows";
export const novu = serve({ workflows: [welcomeWorkflow] });
自定义(任意框架)
import { NovuRequestHandler, ServeHandlerOptions } from "@novu/framework";
export const serve = (options: ServeHandlerOptions) =>
new NovuRequestHandler({
frameworkName: "my-framework",
...options,
handler: (req, res) => ({ /* method, headers, body, url, transformResponse */ }),
}).createHandler();
完整的自定义处理程序签名请参阅 references/bridge-endpoint.md。
本地 Studio
通过公共隧道提供工作流实时预览,让 Novu Cloud 可以访问你的计算机。
npx novu@latest dev
# Defaults: --port 4000 --route /api/novu --studio-port 2022
然后打开 http://localhost:2022(仅限 Chrome)。
CLI 标志
| 标志 | 默认值 | 用途 |
|---|---|---|
-p、--port | 4000 | 你的应用端口 |
-r、--route | /api/novu | Bridge 路由路径 |
-o、--origin | http://localhost | Bridge 源站 |
-d、--dashboard-url | https://dashboard.novu.co | 仪表板 URL——欧盟地区请使用 https://eu.dashboard.novu.co |
-sp、--studio-port | 2022 | Studio UI 端口 |
-t、--tunnel | auto | 可自托管的隧道 URL(例如 ngrok) |
-H、--headless | false | 跳过 Studio UI |
npx novu@latest dev --port 3002 --dashboard-url https://eu.dashboard.novu.co
Studio:
- 自动创建稳定的隧道 URL,例如
https://<id>.novu.sh/api/novu - 允许你编辑步骤控制项和负载,以预览不同状态
- 在
process.env.NODE_ENV=development下运行——关闭 HMAC 验证,以允许 Studio 访问 - 提供“Sync”按钮,可将状态推送到 Cloud(实际部署请使用 CI/CD)
触发工作流
通过代码定义的工作流与通过仪表板定义的工作流触发方式相同——在触发端(服务器、队列工作进程、Webhook 处理程序)使用 @novu/api:
import { Novu } from "@novu/api";
const novu = new Novu({ secretKey: process.env.NOVU_SECRET_KEY });
await novu.trigger({
workflowId: "welcome-email",
to: { subscriberId: "user-123", email: "[email protected]" },
payload: { userName: "Jane", appName: "Acme" },
});
你也可以在另一个工作流的 step.custom 中触发工作流:
await step.custom("trigger-summary", async () => {
return await summaryWorkflow.trigger({
to: subscriber.subscriberId,
payload: { events: events.map(e => e.payload) },
});
});
有关完整的触发选项(批量、广播、主题、覆盖项、transactionId、取消),请参阅 trigger-notification。
React Email(及相关工具)
使用现有组件库渲染电子邮件。
React Email
npm install @react-email/components react-email
import { Body, Container, Head, Html, render } from "@react-email/components";
export const WelcomeEmail = ({ name }: { name: string }) => (
<Html>
<Head />
<Body>
<Container>Hello {name}, welcome!</Container>
</Body>
</Html>
);
export const renderWelcome = (name: string) => render(<WelcomeEmail name={name} />);
await step.email("welcome", async () => ({
subject: "Welcome",
body: renderWelcome(payload.userName),
}));
也支持 Vue Email、Svelte Email 和 Remix + React Email。请参阅 references/email-templates.md。
翻译(i18n)
对于基于框架的工作流,翻译内容保存在代码中(而非面向仪表板工作流的 Novu Translation 系统中)。你可以使用任意 i18n 库(例如 i18next),并在解析器中通过 subscriber.locale 获取内容。
import { workflow } from "@novu/framework";
import i18n from "./i18n";
export const localizedWorkflow = workflow(
"welcome-localized",
async ({ step, subscriber }) => {
await step.email("email", async (controls) => {
const t = i18n.getFixedT([subscriber.locale ?? controls.defaultLocale]);
return {
subject: t("welcomeEmailSubject", { username: subscriber.firstName }),
body: render(<Welcome subject={t("subject")} body={t("body")} />),
};
}, {
controlSchema: z.object({
defaultLocale: z.string().default("en_US"),
}),
});
},
);
完整的 i18next + React Email 示例请参阅 references/translations.md。
标签
为工作流添加标签,以便将其与相关通知归为一组(供收件箱标签页和仪表盘筛选使用):
workflow("login-alert", handler, { tags: ["security"] });
workflow("password-change", handler, { tags: ["security"] });
在收件箱中,使用 tabs={[{ label: "Security", filter: { tags: ["security"] } }]} 渲染一个“安全”标签页(参见 inbox-integration)。
部署
通过 CLI 同步
将工作流推送到 Novu Cloud:
npx novu@latest sync \
--bridge-url https://api.acme.com/api/novu \
--secret-key $NOVU_SECRET_KEY \
--api-url https://api.novu.co # use https://eu.api.novu.co for EU
GitHub Actions
name: Sync Novu Workflows
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: novuhq/actions-novu-sync@v2
with:
secret-key: ${{ secrets.NOVU_SECRET_KEY }}
bridge-url: ${{ secrets.NOVU_BRIDGE_URL }}
api-url: https://api.novu.co
GitOps 工作流
- 在本地使用 Studio 对接自己的机器进行开发。
- 创建 PR — CI 会对 Development 环境运行
npx novu sync,以进行端到端测试。 - 合并到
main— CI 会对 Production 环境运行npx novu sync。
GitLab CI、Jenkins、CircleCI、Bitbucket、Azure DevOps 和 Travis CI 均可通过 CLI 使用。
生产与安全
- Bridge URL 必须可通过 HTTPS 从公网访问。 Novu Cloud 会自动扩缩容 — 不会公布 IP 允许列表。
- 默认启用 HMAC 验证(当
NODE_ENV !== "development"时)。serve包装器会处理此事 — 无需编写任何代码。每个请求都包含一个Novu-Signature标头(t=timestamp,v1=signature),系统会使用NOVU_SECRET_KEY对其进行验证。 - 本地开发时通过
NODE_ENV=development自动禁用 HMAC。 请勿在生产环境中禁用。 - Vercel Preview URL 默认受保护 — 启用 自动化保护绕过,并在 Bridge URL 中通过
?x-vercel-protection-bypass=<token>传入绕过令牌。
自定义客户端
全局覆盖默认设置:
import { Client as NovuFrameworkClient } from "@novu/framework";
import { serve } from "@novu/framework/next";
export const { GET, POST, OPTIONS } = serve({
client: new NovuFrameworkClient({
secretKey: process.env.NOVU_SECRET_KEY,
strictAuthentication: false, // disables HMAC — only for local dev
}),
workflows: [/* … */],
});
客户端读取的环境变量:
NOVU_SECRET_KEY— 你的密钥NOVU_API_URL— 默认为https://api.novu.co(欧盟地区使用https://eu.api.novu.co)
常见问题
- Bridge URL 必须可通过公网访问 — Novu Cloud 无法使用
localhost。本地使用 Studio 隧道;生产环境则部署到公网。 workflowId是触发标识符 — 也是传给novu.trigger({ workflowId })的同一个 ID。请使用 kebab-case,并保持稳定。- 工作流中的步骤
id必须唯一 — 重复的 ID 会在注册时抛出错误。 - JSON Schema 上的
as const— 如果没有它,TS 会将类型推断为string,而不是字面量类型,导致payload变为unknown。 - 每个工作流只能有一个
step.digest— 若需实现两阶段摘要模式,可通过step.custom链接第二个工作流。 - 同一触发产生的摘要 / 延迟结果不会影响其他触发 — 这些结果仅适用于各自的工作流运行。
- 自定义步骤的结果不能用于步骤控制 — 只能在后续步骤的
resolver、providers或skip回调中使用。 - 每次更改工作流后都要同步 — Novu Cloud 需要了解新增 / 重命名的工作流以及更新后的控制架构。请将
npx novu sync添加到 CI/CD 中。 - 当
NODE_ENV !== "development"时,HMAC 在本地会验证失败 — 将其设为development,让 Studio 能够访问你的 Bridge,或者在Client中禁用严格身份验证。 - 不要将
secretKey存储在客户端包中 — 它只能在服务端使用。请将工作流和 Bridge 路由保留在服务端代码中,不要放在任何"use client"模块中。 - 提供商覆盖项
_passthrough不会经过验证 — 拼写错误不会在编译时触发错误。请尽可能使用已知的、带类型的提供商键。 - 更改延迟 / 摘要步骤的内容不会影响已安排的事件 — 内容会在原始触发时保存。
- 工作流处理程序在重试时必须具有确定性 — Novu 会重新调用 Bridge 来解析步骤内容。请避免在
step.custom之外产生副作用(只有 custom 步骤的结果会被持久化保存)。 @novu/framework要求 Node.js ≥ 20。
代码风格提示
- 每个工作流对应
src/novu/workflows/<workflow-id>.ts下的一个文件,并从总导出文件src/novu/workflows/index.ts重新导出。 - 优先使用 Zod 架构,自动补全和类型推断效果最佳。仅在需要 Zod 未提供的功能(
oneOf、if/then/else、$ref)时使用 JSON Schema。 - 将 React Email 模板与使用它的工作流放在一起(
src/novu/workflows/welcome/template.tsx)。 - 将共享的
step.custom逻辑封装为辅助函数(fetchUser(payload.userId)),以便复用。 - 对于 NestJS,使用
NovuModule.registerAsync和NotificationService,以便工作流定义可以注入服务。
参考资料
- Bridge Endpoint 设置 — 每种框架封装、自定义
serve、NestJS DI - 工作流与步骤 API — 完整选项、所有步骤类型、条件逻辑模式
- 架构验证 — Zod、JSON Schema、Class Validator 深入介绍
- 电子邮件模板 — React、Vue、Svelte Email 集成
- 翻译 — 基于 i18next 的本地化工作流
- 本地 Studio 与 CLI — 所有标志、隧道模式、无头模式
- 部署 —
npx novu sync、GitHub Action、GitOps 配方、EU 区域 - 生产环境与安全 — HMAC、公共 bridge 要求、Vercel 预览绕过
- 示例食谱 — 多步骤引导、摘要、延迟后跳过、由大模型驱动的摘要