novu-framework-integration について
`@novu/framework`でアプリケーションコード内にNovuの通知ワークフローを定義する開発者向けスキルです。型付きペイロード、チャネルやアクションのステップ、Novu Cloudが内容とデータを取得するBridge Endpointを説明します。
主な機能
- 01アプリケーションコードでワークフローを定義
- 02トリガーペイロードの検証と型付け
- 03チャネルとアクションのステップ構築
- 04編集可能なステップ用スキーマの追加
- 05Bridge 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 に保存する必要がある | コンテンツ全般を技術者以外のチームメンバーが管理する |
| DB/API から最新データを取得する必要がある | すべてのデータがトリガーペイロードに収まる |
| React/Vue/Svelte Email でメールをレンダリングする | ブロックエディターで十分である |
| カスタムコード(LLM、サードパーティ API)を実行する | 送信のみのシンプルなフローである |
| 型付きペイロードとステップ制御が必要である | すばやくプロトタイプを作成する |
2 つの方法は併用できます。1 つの環境で、コード定義のワークフローと 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>
これにより、ワークフロー、環境ファイル、動作する /api/novu ルートを備えたサンプルの Bridge アプリが作成されます。
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 にアクセスできます。
ワークフローの構成
ワークフロー自体を設計していますか?チャネルの選択、重要度、
critical、ダイジェストのデフォルト設定、ステップ条件、および 9 つの参照テンプレート(注文確認、支払い失敗、アカウント停止、コメント、トライアル終了間近、パスワードリセット、Webhook ファンアウト、取得後に通知)については、design-workflow/をご覧ください。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 } |
メールステップ
await step.email("welcome", async (controls) => ({
subject: controls.subject,
body: render(<WelcomeEmail name={subscriber.firstName} />),
from: "[email protected]",
replyTo: "[email protected]",
}));
アプリ内ステップ(リッチペイロード)
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 },
}));
アプリ内ステップは { seen, read, lastSeenDate, lastReadDate } を返します。後続のステップの skip を制御するために使用してください。
SMS/プッシュ/チャット
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
一定期間に複数のトリガーを集約し、1 つの通知にまとめます。
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 ベースのダイジェストでは、unit/amount の代わりに cron: "0 0 * * *" を使用してください。各ダイジェストイベントには { id, time, payload } が含まれます。1 つのワークフローにつき、ダイジェストは 1 つだけです。2 段階のダイジェストが必要な場合は、step.custom を使って 2 つ目のワークフローにつないでください。
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 やエスケープされていない文字(アプリ内の 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,
});
JSON Schema から TS がペイロードの型を推論するには、
as constが必要です。
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にすると、ワークフローは重要になり、サブスクライバーは無効にできません。all.enabledは、channelsに含まれていないチャネルのフォールバックになります。- 省略時のデフォルト:すべてのチャネルで
enabled: true、readOnly: false。
Bridge エンドポイントの設定
Bridge は単一の HTTP ルート(デフォルトでは /api/novu)で、Novu Cloud がアプリを呼び出して以下を実行します。
- 登録済みワークフローを検出する(
GET) - 指定したサブスクライバーとペイロードのステップコンテンツを解決する(
POST) - リクエストの HMAC 署名を検証する
各フレームワークには、解析、HMAC 検証、レスポンスの整形を処理する serve ラッパーが用意されています。
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 — EU の場合は 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 では以下を行えます。
https://<id>.novu.sh/api/novuのような安定したトンネル URL を自動作成する- ステップのコントロールとペイロードを編集して、さまざまな状態をプレビューする
process.env.NODE_ENV=developmentに対して実行する — Studio からのアクセスを許可するため、HMAC 検証は無効です- 状態を Cloud にプッシュする「Sync」ボタンを利用する(実際のデプロイには 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)
Framework ベースのワークフローでは、翻訳はコード内で管理します(ダッシュボードのワークフローを対象とする Novu の翻訳システムは使用しません)。i18next などの任意の i18n ライブラリを使用し、リゾルバー内で 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 を参照してください。
タグ
ワークフローにタグを付けて、関連する通知をグループ化します(Inbox のタブと Dashboard のフィルタリングで使用):
workflow("login-alert", handler, { tags: ["security"] });
workflow("password-change", handler, { tags: ["security"] });
Inbox に tabs={[{ label: "Security", filter: { tags: ["security"] } }]} を使って「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を実行し、E2E テストを行います。 mainにマージします。CI は Production 環境に対してnpx novu syncを実行します。
GitLab CI、Jenkins、CircleCI、Bitbucket、Azure DevOps、Travis CI も CLI 経由で利用できます。
本番環境とセキュリティ
- Bridge URL は HTTPS 経由でパブリックにアクセス可能である必要があります。 Novu Cloud は自動でスケールするため、IP 許可リストは公開されていません。
NODE_ENV !== "development"の場合、HMAC 検証はデフォルトで有効です。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: [/* … */],
});
Client が読み取る環境変数:
NOVU_SECRET_KEY— シークレットキーNOVU_API_URL— デフォルトはhttps://api.novu.coです(EU の場合はhttps://eu.api.novu.coを使用します)
よくある落とし穴
- Bridge URL はパブリックにアクセス可能である必要があります。 Novu Cloud では
localhostは使用できません。ローカルでは Studio トンネルを使用し、本番環境ではパブリックにデプロイしてください。 workflowIdはトリガー識別子です。novu.trigger({ workflowId })に渡すものと同じ ID です。ケバブケースを使用し、安定した値を維持してください。- ステップの
idはワークフロー内で一意である必要があります。 重複すると登録時にエラーが発生します。 - JSON Schema には
as constを指定してください。 指定しないと、TS はstringとして推論し、payloadがunknownになります。 - 1 つのワークフローで使用できる
step.digestは 1 つだけです。 2 段階のダイジェストパターンでは、step.customを使って 2 つ目のワークフローを連鎖させてください。 - 1 つのトリガーのダイジェスト/遅延の結果は、他のトリガーに影響しません。 これらはワークフローの実行ごとに適用されます。
- カスタムステップの結果はステップ制御では使用できません。 後続ステップの
resolver、providers、またはskipコールバックでのみ使用できます。 - ワークフローを変更するたびに同期してください。 Novu Cloud が新規/改名されたワークフローや更新された制御スキーマを認識する必要があります。CI/CD に
npx novu syncを追加してください。 NODE_ENV !== "development"の場合、ローカルでは HMAC に失敗します。 Studio から Bridge にアクセスするにはdevelopmentに設定するか、Clientで厳格な認証を無効にしてください。- クライアントバンドルに
secretKeyを保存しないでください。 これはサーバー専用です。ワークフローと Bridge ルートは、"use client"モジュールではなくサーバーコード内に保持してください。 - プロバイダーの上書き
_passthroughは検証されません。 タイプミスがあってもコンパイル時にエラーになりません。可能な限り、型が定義された既知のプロバイダーキーを使用してください。 - 遅延/ダイジェストステップの内容を変更しても、すでにスケジュールされたイベントには影響しません。 内容は元のトリガー時に取得されます。
- ワークフローハンドラーは、リトライ間で決定的である必要があります。 Novu はステップの内容を解決するために Bridge を再呼び出しします。
step.customの外部で副作用を発生させないでください(結果が永続的に保存されるのはcustomだけです)。 @novu/frameworkには Node.js ≥ 20 が必要です。
コードスタイルのヒント
src/novu/workflows/<workflow-id>.tsにワークフローごとに1つのファイルを作成し、バレルファイル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 では、ワークフロー定義からサービスを注入できるよう、
NotificationServiceとNovuModule.registerAsyncを使用します。
参考資料
- ブリッジエンドポイントの設定 — 各フレームワークのラッパー、カスタムの
serve、NestJS の DI - ワークフローとステップの API — オプションの詳細、すべてのステップタイプ、条件付きロジックのパターン
- スキーマ検証 — Zod、JSON Schema、Class Validator の詳細解説
- メールテンプレート — React、Vue、Svelte Email との連携
- 翻訳 — i18next ベースのローカライズされたワークフロー
- ローカル Studio と CLI — すべてのフラグ、トンネルモード、ヘッドレスモード
- デプロイ —
npx novu sync、GitHub Action、GitOps のレシピ、EU リージョン - 本番環境とセキュリティ — HMAC、公開ブリッジの要件、Vercel プレビューのバイパス
- サンプル集 — 複数ステップのオンボーディング、ダイジェスト、遅延後のスキップ、LLM を活用したダイジェスト