mcpField
N
novu-framework-integration

novu-framework-integration

github.com/novuhq開発4466 回インストール19 週間前に更新
リポジトリ

novu-framework-integration について

`@novu/framework`でアプリケーションコード内にNovuの通知ワークフローを定義する開発者向けスキルです。型付きペイロード、チャネルやアクションのステップ、Novu Cloudが内容とデータを取得するBridge Endpointを説明します。

主な機能

  1. 01アプリケーションコードでワークフローを定義
  2. 02トリガーペイロードの検証と型付け
  3. 03チャネルとアクションのステップ構築
  4. 04編集可能なステップ用スキーマの追加
  5. 05Bridge Endpointによるワークフロー公開

活用シーン

  1. 01通知ワークフローをソース管理する
  2. 02実行時に最新データを取得する
  3. 03チームメンバーがDashboardでステップ内容を編集する
  4. 04遅延、ダイジェスト、HTTPリクエストを組み合わせる

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/novuhq/skills --skill novu-framework-integration

インストール後、Claude Code で/novu-framework-integrationを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
bridge-endpoint.md
deployment.md
email-templates.md
examples.md
schema-validation.md
security.md
studio-and-cli.md
translations.md
workflow-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 定義のワークフローの両方を利用できます。

仕組み

  1. @novu/framework の workflow(...) を使って、コードでワークフローを定義します。
  2. アプリに単一の /api/novu HTTP ルート(Bridge エンドポイント)を公開します。
  3. Bridge の URL を Novu Cloud に同期します(npx novu sync または GitHub Action を使用)。
  4. ワークフローの実行中、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);
パラメーター型説明
workflowIdstring環境内で一意の識別子
handler({ step, payload, subscriber }) => Promise<void>ワークフロー本体。ステップを順番に呼び出します
optionsWorkflowOptionsスキーマ、名前、説明、タグ、設定
ワークフローのオプション
オプション型目的
payloadSchemaZodSchema | JsonSchema | ClassValidatorClassトリガーペイロードを検証し、payload の TS 型を推論します
namestringDashboard/<Inbox /> に表示される、人が読める名前
descriptionstringDashboard に表示される説明
tagsstring[]フィルタリング/Inbox タブ用の分類
severity'low' | 'medium' | 'high'Inbox での視覚的な優先順位。ほとんどのワークフローでは設定しないでください。
criticalboolean購読者の設定を無視し、ダイジェストをスキップして、遅延なしで実行します。必ず配信すべきイベントにのみ使用してください。
preferencesWorkflowPreferencesデフォルトのチャネル設定と 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.emailsubject、bodyattachments、from、replyToいいえ
step.smsbody—いいえ
step.pushtitle(または subject)、bodydata、image、iconいいえ
step.chatbody—(プロバイダーごとに上書き)いいえ
step.inAppbodysubject、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、--port4000アプリのポート
-r、--route/api/novuBridge のルートパス
-o、--originhttp://localhostBridge のオリジン
-d、--dashboard-urlhttps://dashboard.novu.coダッシュボードの URL — EU の場合は https://eu.dashboard.novu.co を使用
-sp、--studio-port2022Studio UI のポート
-t、--tunnelautoセルフホストのトンネル URL(例:ngrok)
-H、--headlessfalseStudio 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 ワークフロー
  1. Studio を使って、ローカルで自身のマシンに対して開発します。
  2. PR を作成します。CI は Development 環境に対して npx novu sync を実行し、E2E テストを行います。
  3. 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 を使用します)

よくある落とし穴

  1. Bridge URL はパブリックにアクセス可能である必要があります。 Novu Cloud では localhost は使用できません。ローカルでは Studio トンネルを使用し、本番環境ではパブリックにデプロイしてください。
  2. workflowId はトリガー識別子です。 novu.trigger({ workflowId }) に渡すものと同じ ID です。ケバブケースを使用し、安定した値を維持してください。
  3. ステップの id はワークフロー内で一意である必要があります。 重複すると登録時にエラーが発生します。
  4. JSON Schema には as const を指定してください。 指定しないと、TS は string として推論し、payload が unknown になります。
  5. 1 つのワークフローで使用できる step.digest は 1 つだけです。 2 段階のダイジェストパターンでは、step.custom を使って 2 つ目のワークフローを連鎖させてください。
  6. 1 つのトリガーのダイジェスト/遅延の結果は、他のトリガーに影響しません。 これらはワークフローの実行ごとに適用されます。
  7. カスタムステップの結果はステップ制御では使用できません。 後続ステップの resolver、providers、または skip コールバックでのみ使用できます。
  8. ワークフローを変更するたびに同期してください。 Novu Cloud が新規/改名されたワークフローや更新された制御スキーマを認識する必要があります。CI/CD に npx novu sync を追加してください。
  9. NODE_ENV !== "development" の場合、ローカルでは HMAC に失敗します。 Studio から Bridge にアクセスするには development に設定するか、Client で厳格な認証を無効にしてください。
  10. クライアントバンドルに secretKey を保存しないでください。 これはサーバー専用です。ワークフローと Bridge ルートは、"use client" モジュールではなくサーバーコード内に保持してください。
  11. プロバイダーの上書き _passthrough は検証されません。 タイプミスがあってもコンパイル時にエラーになりません。可能な限り、型が定義された既知のプロバイダーキーを使用してください。
  12. 遅延/ダイジェストステップの内容を変更しても、すでにスケジュールされたイベントには影響しません。 内容は元のトリガー時に取得されます。
  13. ワークフローハンドラーは、リトライ間で決定的である必要があります。 Novu はステップの内容を解決するために Bridge を再呼び出しします。step.custom の外部で副作用を発生させないでください(結果が永続的に保存されるのは custom だけです)。
  14. @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 を使用します。

参考資料

コメント