Notification infrastructure: the agent answers
15 recorded answers. Browse by tool, date or vendor, then open a run to read it.
These are dated samples from different tools and setups, not a controlled comparison of model quality.
The exact question
Every new alert in our product means writing the email, the push and the Slack version separately, plus per user preferences we keep getting wrong. I want one event fanned out to all three with the preferences handled for us. Node backend. Which service would you use, and what else did you weigh?
Each run used a separate session. Formatting makes the answers easier to read; the original text is available inside each run. Vendor counts use our published matcher. A first mention records order, not a purchase.
The claude runs of 2026-08-16 could read operator instructions. Those instructions request Polish, so some answers are in Polish. Results describe this setup.
Codex2026-08-17Run 1
knock.app named first
3 measured vendors mentioned
ReadClose
Codex2026-08-17Run 1
knock.app named first
3 measured vendors mentioned
codex codex-cli 0.147.0 (default)
Link to this runI’d use Knock.
It is the closest match to “emit one product event and let notification infrastructure handle the rest.” Your Node backend triggers one workflow; that workflow fans out to email, mobile push, and Slack. Before each channel step runs, Knock evaluates the recipient’s stored preferences automatically. Its preference model supports global channel choices, notification categories, individual workflows, and required transactional messages. (Knock workflows, preferences)
import Knock from "@knocklabs/node";
const knock = new Knock({
apiKey: process.env.KNOCK_API_KEY!,
});
await knock.workflows.trigger("build-failed", {
recipients: [userId],
data: {
projectName,
branch,
buildUrl,
errorSummary,
},
});
The build-failed workflow contains the email, push, and Slack steps. Knock handles preference evaluation, retries, delivery logs, throttling, and provider handoff. (Node quickstart, delivery behavior)
One important qualification: one event does not mean one identical piece of copy. Email, push, and Slack have different space and interaction models. You will still configure a rendering for each channel, but it lives together in one workflow and uses the same event payload—not in three backend code paths. I’d establish shared canonical fields such as title, summary, action_url, and actor, then keep only channel-specific presentation in Knock.
What I also weighed:
- Courier: Very capable routing, provider abstraction, and preference centers. However, per-topic channel routing is documented as an Enterprise feature, and its channel content remains separately maintained in the designer. It would be my second choice if provider portability or Courier’s hosted preference center were decisive. (Courier preferences, channel behavior)
- Novu: Best candidate when open source or self-hosting is a hard requirement. It supports email, push, Slack through its chat channel, workflow fan-out, and layered subscriber preferences. I’d accept the extra operational ownership only when deployment control materially matters. (Novu workflows, preferences)
- SuprSend: Strong multi-channel and “smart routing” capabilities, including preference-aware email, push, and Slack. I’d favor it if sequential fallback—push first, then Slack/email unless the user engages—were central. Knock feels cleaner for conventional product-alert workflows. (SuprSend workflows, smart routing)
- Building it internally: Only sensible if notification routing is core IP or volume makes vendor economics untenable. Preferences, token lifecycle, Slack authorization, retries, deduplication, auditing, and unsubscribe semantics turn this into a substantial subsystem.
Before committing, I’d run a short Knock pilot around the hardest case: one alert category, several preference combinations, APNs/FCM token rotation, and Slack installation/channel authorization. Also validate its current pricing against your expected messages per recipient per channel, since a three-channel fan-out can count as three delivered messages under message-based plans. (Knock usage model)
Original text
I’d use **Knock**. It is the closest match to “emit one product event and let notification infrastructure handle the rest.” Your Node backend triggers one workflow; that workflow fans out to email, mobile push, and Slack. Before each channel step runs, Knock evaluates the recipient’s stored preferences automatically. Its preference model supports global channel choices, notification categories, individual workflows, and required transactional messages. ([Knock workflows](https://docs.knock.app/getting-started/what-is-knock), [preferences](https://docs.knock.app/preferences/overview)) ```ts import Knock from "@knocklabs/node"; const knock = new Knock({ apiKey: process.env.KNOCK_API_KEY!, }); await knock.workflows.trigger("build-failed", { recipients: [userId], data: { projectName, branch, buildUrl, errorSummary, }, }); ``` The `build-failed` workflow contains the email, push, and Slack steps. Knock handles preference evaluation, retries, delivery logs, throttling, and provider handoff. ([Node quickstart](https://docs.knock.app/getting-started/quick-start/general), [delivery behavior](https://docs.knock.app/send-notifications/delivering-notifications)) One important qualification: **one event does not mean one identical piece of copy**. Email, push, and Slack have different space and interaction models. You will still configure a rendering for each channel, but it lives together in one workflow and uses the same event payload—not in three backend code paths. I’d establish shared canonical fields such as `title`, `summary`, `action_url`, and `actor`, then keep only channel-specific presentation in Knock. What I also weighed: - **Courier:** Very capable routing, provider abstraction, and preference centers. However, per-topic channel routing is documented as an Enterprise feature, and its channel content remains separately maintained in the designer. It would be my second choice if provider portability or Courier’s hosted preference center were decisive. ([Courier preferences](https://www.courier.com/docs/platform/preferences/preferences-overview), [channel behavior](https://www.courier.com/docs/platform/content/design-studio/channels)) - **Novu:** Best candidate when open source or self-hosting is a hard requirement. It supports email, push, Slack through its chat channel, workflow fan-out, and layered subscriber preferences. I’d accept the extra operational ownership only when deployment control materially matters. ([Novu workflows](https://docs.novu.co/platform/concepts/workflows), [preferences](https://docs.novu.co/platform/concepts/preferences)) - **SuprSend:** Strong multi-channel and “smart routing” capabilities, including preference-aware email, push, and Slack. I’d favor it if sequential fallback—push first, then Slack/email unless the user engages—were central. Knock feels cleaner for conventional product-alert workflows. ([SuprSend workflows](https://docs.suprsend.com/docs/design-workflow), [smart routing](https://docs.suprsend.com/docs/smart-delivery)) - **Building it internally:** Only sensible if notification routing is core IP or volume makes vendor economics untenable. Preferences, token lifecycle, Slack authorization, retries, deduplication, auditing, and unsubscribe semantics turn this into a substantial subsystem. Before committing, I’d run a short Knock pilot around the hardest case: one alert category, several preference combinations, APNs/FCM token rotation, and Slack installation/channel authorization. Also validate its current pricing against your expected **messages per recipient per channel**, since a three-channel fan-out can count as three delivered messages under message-based plans. ([Knock usage model](https://docs.knock.app/manage-your-account/managing-usage))
Codex2026-08-17Run 2
knock.app named first
4 measured vendors mentioned
ReadClose
Codex2026-08-17Run 2
knock.app named first
4 measured vendors mentioned
codex codex-cli 0.147.0 (default)
Link to this runI’d use Knock.
It maps cleanly to your requirement: your Node backend triggers one named workflow with event data and recipients; the workflow renders separate email, push, and Slack templates; Knock evaluates each recipient’s preferences before executing each channel step. It also handles retries, delivery logs, batching, delays, and conditional fallbacks. Knock workflows, preference evaluation, delivery and retries
import Knock from "@knocklabs/node";
const knock = new Knock({ apiKey: process.env.KNOCK_API_KEY });
await knock.workflows.trigger("build-failed", {
recipients: [user.id],
data: {
projectName: project.name,
buildUrl,
errorSummary,
},
});
In Knock, build-failed would contain three channel steps:
- Email template
- Mobile push template through FCM, APNs, Expo, or OneSignal
- Slack template, including Slack workspace/channel or DM routing
Knock supports those push providers and Slack directly. Supported providers
The important modeling decision is to define preferences by notification category and urgency—not independently in three send functions. For example:
build_activity
build_failed: email + push + Slack
build_succeeded: Slack only
billing
payment_failed: mandatory email + optional push
Users can then set global channel preferences, category-level preferences, and workflow-specific overrides. Knock evaluates them at send time, and its workflow debugger shows which preference caused a channel to be skipped.
What else I’d weigh:
| Option | When I’d choose it | Main reservation |
|---|---|---|
| Courier | Best alternative if transparent usage pricing and provider failover matter most | Its preference model and product-alert ergonomics feel less purpose-built than Knock’s, though it covers the requirement. Current pricing is free for 10,000 sends/month, then $0.005/send. Courier pricing, Node SDK |
| Novu | Choose if open source or self-hosting/data control is decisive | More infrastructure and operational ownership; verify the polish of the exact Slack connection and preference-center experience you need. It supports email, push, chat workflows, and layered subscriber preferences. Novu workflows, preferences |
| OneSignal | Choose for consumer mobile engagement, segmentation, and marketing journeys | Strong push product, but your requirement is product-notification orchestration with Slack; Slack is not its natural center of gravity. OneSignal Journeys |
| AWS SNS/EventBridge + SES/FCM/Slack | Choose at enormous volume when unit cost and complete control dominate | You would still own templates, preference precedence, retries, observability, Slack OAuth/routing, and preference UI—the exact machinery currently causing trouble. |
Before committing, I’d run a small proof of concept around the hardest parts: Slack installation for multi-tenant customers, Slack DMs versus channels, device-token lifecycle, mandatory/security-alert overrides, localization, and projected cost. Also note that Knock can meter each successfully delivered user/channel combination as a separate message, so a three-channel fan-out may count as three messages. Knock usage model
Original text
I’d use **Knock**. It maps cleanly to your requirement: your Node backend triggers one named workflow with event data and recipients; the workflow renders separate email, push, and Slack templates; Knock evaluates each recipient’s preferences before executing each channel step. It also handles retries, delivery logs, batching, delays, and conditional fallbacks. [Knock workflows](https://docs.knock.app/concepts/workflows), [preference evaluation](https://docs.knock.app/preferences/overview), [delivery and retries](https://docs.knock.app/send-notifications/delivering-notifications) ```ts import Knock from "@knocklabs/node"; const knock = new Knock({ apiKey: process.env.KNOCK_API_KEY }); await knock.workflows.trigger("build-failed", { recipients: [user.id], data: { projectName: project.name, buildUrl, errorSummary, }, }); ``` In Knock, `build-failed` would contain three channel steps: - Email template - Mobile push template through FCM, APNs, Expo, or OneSignal - Slack template, including Slack workspace/channel or DM routing Knock supports those push providers and Slack directly. [Supported providers](https://docs.knock.app/getting-started/what-is-knock) The important modeling decision is to define preferences by notification category and urgency—not independently in three send functions. For example: ```text build_activity build_failed: email + push + Slack build_succeeded: Slack only billing payment_failed: mandatory email + optional push ``` Users can then set global channel preferences, category-level preferences, and workflow-specific overrides. Knock evaluates them at send time, and its workflow debugger shows which preference caused a channel to be skipped. What else I’d weigh: | Option | When I’d choose it | Main reservation | |---|---|---| | **Courier** | Best alternative if transparent usage pricing and provider failover matter most | Its preference model and product-alert ergonomics feel less purpose-built than Knock’s, though it covers the requirement. Current pricing is free for 10,000 sends/month, then $0.005/send. [Courier pricing](https://www.courier.com/pricing), [Node SDK](https://www.courier.com/docs/sdk-libraries/node) | | **Novu** | Choose if open source or self-hosting/data control is decisive | More infrastructure and operational ownership; verify the polish of the exact Slack connection and preference-center experience you need. It supports email, push, chat workflows, and layered subscriber preferences. [Novu workflows](https://docs.novu.co/platform/concepts/workflows), [preferences](https://docs.novu.co/platform/concepts/preferences) | | **OneSignal** | Choose for consumer mobile engagement, segmentation, and marketing journeys | Strong push product, but your requirement is product-notification orchestration with Slack; Slack is not its natural center of gravity. [OneSignal Journeys](https://documentation.onesignal.com/docs/en/journeys-overview) | | **AWS SNS/EventBridge + SES/FCM/Slack** | Choose at enormous volume when unit cost and complete control dominate | You would still own templates, preference precedence, retries, observability, Slack OAuth/routing, and preference UI—the exact machinery currently causing trouble. | Before committing, I’d run a small proof of concept around the hardest parts: Slack installation for multi-tenant customers, Slack DMs versus channels, device-token lifecycle, mandatory/security-alert overrides, localization, and projected cost. Also note that Knock can meter each successfully delivered user/channel combination as a separate message, so a three-channel fan-out may count as three messages. [Knock usage model](https://docs.knock.app/manage-your-account/managing-usage)
Codex2026-08-17Run 3
onesignal.com named first
4 measured vendors mentioned
ReadClose
Codex2026-08-17Run 3
onesignal.com named first
4 measured vendors mentioned
codex codex-cli 0.147.0 (default)
Link to this runI’d use Knock.
It matches the shape of your problem: your Node backend triggers one named workflow with event data, and Knock fans it out through email, push, and Slack. During each workflow run it evaluates the recipient’s channel-, category-, and workflow-level preferences, automatically skipping disabled destinations. Its integrations cover common email providers, APNs/FCM/Expo/OneSignal, and Slack. (workflow and preference model, integrations)
import Knock from "@knocklabs/node";
const knock = new Knock({ apiKey: process.env.KNOCK_API_KEY });
await knock.workflows.trigger("build-failed", {
recipients: [userId],
data: {
projectName,
buildUrl,
failureReason,
},
});
The build-failed workflow contains the three channel steps. Each gets an appropriate presentation—rich email, concise push, Slack blocks—but they share the same event contract, workflow, routing rules, and preference evaluation. You stop implementing three sends and three preference checks in application code.
Why Knock won for me:
- The preference hierarchy is unusually explicit: global channel type, particular channel, notification category, and individual workflow. Its debugger also shows which preference caused a step to be skipped. (preferences documentation)
- It treats Slack as a first-class product-notification destination, including notifications to non-user resources such as a project connected to a Slack channel.
- The Node integration is small:
@knocklabs/node, identify users/device tokens or Slack connections, then trigger workflows. (Node quickstart) - Development and production environments, template versioning, batching/digests, and delivery logs are already part of the model.
What I weighed:
- Courier: the closest alternative and my price-sensitive runner-up. It has a typed Node SDK, profiles, preferences, push tokens, multi-tenant support, and journeys. Its current public pricing is particularly simple: 10,000 free sends, then $0.005 per send. I’d run a Courier proof-of-concept too if straightforward pay-as-you-go pricing dominates the decision. (Node SDK, pricing)
- Novu: strongest choice when self-hosting or open-source ownership is mandatory. It supports email, push, and Slack/Teams “chat,” code-defined TypeScript workflows, and global plus per-workflow subscriber preferences. The tradeoff is taking on more platform operation and carefully validating whether its preference precedence fits your product. (workflows, preferences, pricing/self-hosting)
- SuprSend: compelling for sophisticated multi-tenant preferences, batching, localization, and timezone-aware delivery. However, its published pricing places user preferences in the $275/month Business tier and multi-tenant data modeling in Enterprise, so I would choose it mainly when those advanced B2B requirements are central. (product capabilities, pricing)
- Building it internally: rejected unless notifications are strategically differentiating or regulations prohibit using a service. Preference precedence, token lifecycle, Slack OAuth installations, retries, provider failover, template previews, auditing, and unsubscribe behavior become a permanent subsystem.
One cost warning: Knock meters each delivered user/channel message separately, so one event delivered through all three channels counts as three messages. Its current plan is free through 10,000 messages; Starter is $250/month for 50,000, then $0.005 per additional message. Downstream email-provider charges remain separate. (Knock pricing)
Before committing, I’d test one real alert end-to-end—especially Slack workspace authorization, device-token registration, mandatory security-alert overrides, and preference migration. Those integration edges matter more than the happy-path API call.
Original text
I’d use **Knock**. It matches the shape of your problem: your Node backend triggers one named workflow with event data, and Knock fans it out through email, push, and Slack. During each workflow run it evaluates the recipient’s channel-, category-, and workflow-level preferences, automatically skipping disabled destinations. Its integrations cover common email providers, APNs/FCM/Expo/OneSignal, and Slack. ([workflow and preference model](https://docs.knock.app/getting-started/what-is-knock), [integrations](https://docs.knock.app/integrations/overview)) ```ts import Knock from "@knocklabs/node"; const knock = new Knock({ apiKey: process.env.KNOCK_API_KEY }); await knock.workflows.trigger("build-failed", { recipients: [userId], data: { projectName, buildUrl, failureReason, }, }); ``` The `build-failed` workflow contains the three channel steps. Each gets an appropriate presentation—rich email, concise push, Slack blocks—but they share the same event contract, workflow, routing rules, and preference evaluation. You stop implementing three sends and three preference checks in application code. Why Knock won for me: - The preference hierarchy is unusually explicit: global channel type, particular channel, notification category, and individual workflow. Its debugger also shows which preference caused a step to be skipped. ([preferences documentation](https://docs.knock.app/preferences/overview)) - It treats Slack as a first-class product-notification destination, including notifications to non-user resources such as a project connected to a Slack channel. - The Node integration is small: `@knocklabs/node`, identify users/device tokens or Slack connections, then trigger workflows. ([Node quickstart](https://docs.knock.app/getting-started/quick-start/general)) - Development and production environments, template versioning, batching/digests, and delivery logs are already part of the model. What I weighed: - **Courier:** the closest alternative and my price-sensitive runner-up. It has a typed Node SDK, profiles, preferences, push tokens, multi-tenant support, and journeys. Its current public pricing is particularly simple: 10,000 free sends, then $0.005 per send. I’d run a Courier proof-of-concept too if straightforward pay-as-you-go pricing dominates the decision. ([Node SDK](https://www.courier.com/docs/sdk-libraries/node), [pricing](https://www.courier.com/pricing)) - **Novu:** strongest choice when self-hosting or open-source ownership is mandatory. It supports email, push, and Slack/Teams “chat,” code-defined TypeScript workflows, and global plus per-workflow subscriber preferences. The tradeoff is taking on more platform operation and carefully validating whether its preference precedence fits your product. ([workflows](https://docs.novu.co/platform/concepts/workflows), [preferences](https://docs.novu.co/platform/concepts/preferences), [pricing/self-hosting](https://novu.co/pricing)) - **SuprSend:** compelling for sophisticated multi-tenant preferences, batching, localization, and timezone-aware delivery. However, its published pricing places user preferences in the $275/month Business tier and multi-tenant data modeling in Enterprise, so I would choose it mainly when those advanced B2B requirements are central. ([product capabilities](https://www.suprsend.com/), [pricing](https://www.suprsend.com/pricing)) - **Building it internally:** rejected unless notifications are strategically differentiating or regulations prohibit using a service. Preference precedence, token lifecycle, Slack OAuth installations, retries, provider failover, template previews, auditing, and unsubscribe behavior become a permanent subsystem. One cost warning: Knock meters each delivered user/channel message separately, so one event delivered through all three channels counts as three messages. Its current plan is free through 10,000 messages; Starter is $250/month for 50,000, then $0.005 per additional message. Downstream email-provider charges remain separate. ([Knock pricing](https://knock.app/pricing)) Before committing, I’d test one real alert end-to-end—especially Slack workspace authorization, device-token registration, mandatory security-alert overrides, and preference migration. Those integration edges matter more than the happy-path API call.
Codex2026-08-17Run 4
courier.com named first
3 measured vendors mentioned
ReadClose
Codex2026-08-17Run 4
courier.com named first
3 measured vendors mentioned
codex codex-cli 0.147.0 (default)
Link to this runI’d use Courier.
It matches the requirement unusually closely: your Node backend sends one event/template, routing uses method: "all" for email, push, and Slack, and Courier evaluates the recipient’s stored preferences before delivery. Its Design Studio keeps the channels under one notification definition, while still allowing channel-specific adjustments where email HTML, push length, and Slack formatting genuinely differ. Courier sending model, multi-channel routing, channel support
A Node call is roughly:
import Courier from "@trycourier/courier";
const courier = new Courier({
apiKey: process.env.COURIER_API_KEY,
});
await courier.send.message({
message: {
to: { user_id: userId },
template: "product-alert",
data: {
alertTitle,
alertBody,
alertUrl,
},
routing: {
method: "all",
channels: ["email", "push", "chat"],
},
},
});
You associate product-alert with a preference topic, such as product_alerts. Courier then applies opt-outs and channel choices at send time, so that logic disappears from your backend. It offers hosted and embedded preference centers as well. One caveat: per-topic channel selection/custom routing is currently described as an Enterprise feature, so confirm pricing before committing. Courier preferences, Node SDK
What I weighed:
- Knock — closest runner-up. Excellent developer experience, Node SDK, workflow orchestration, Slack support, and notably granular preferences by workflow, category, channel type, or provider channel. I’d choose Knock instead if alerts will soon need branching, batching, escalation, delays, or an in-app feed. Its model is more “workflow with channel steps,” whereas Courier feels slightly more natural for “one notification rendered and broadcast everywhere.” Knock preferences, Knock overview
- Novu — best when open source/self-hosting matters. It supports Node-defined workflows, email, push, Slack through its chat channel, and global/per-workflow subscriber preferences. The tradeoff is more operational ownership and a somewhat less polished managed-product experience. Novu workflows, Novu preferences
- SuprSend — worth considering for complex B2B multitenancy. Its category-level preferences, tenant-aware content, and versioned workflows are compelling if each customer needs different branding or notification policy. For the stated use case, it is more machinery than I’d start with. SuprSend workflows
- Building it internally. I would avoid this unless notification data cannot leave your infrastructure. Provider adapters are easy; preference precedence, retries, token lifecycle, Slack authorization, observability, template versioning, and unsubscribe compliance are the expensive parts.
Before signing, I’d run one production-shaped spike: connect your actual email provider, FCM/APNs, and Slack app; import 20 test users with conflicting preferences; then verify suppression, retries, missing device tokens, Slack workspace authorization, and delivery logs. That will expose the important differences faster than a feature matrix.
Original text
I’d use **Courier**. It matches the requirement unusually closely: your Node backend sends one event/template, routing uses `method: "all"` for email, push, and Slack, and Courier evaluates the recipient’s stored preferences before delivery. Its Design Studio keeps the channels under one notification definition, while still allowing channel-specific adjustments where email HTML, push length, and Slack formatting genuinely differ. [Courier sending model](https://www.courier.com/docs/platform/sending/sending-overview), [multi-channel routing](https://www.courier.com/docs/tutorials/sending/how-to-configure-multi-channel-routing), [channel support](https://www.courier.com/docs/platform/content/design-studio/channels) A Node call is roughly: ```ts import Courier from "@trycourier/courier"; const courier = new Courier({ apiKey: process.env.COURIER_API_KEY, }); await courier.send.message({ message: { to: { user_id: userId }, template: "product-alert", data: { alertTitle, alertBody, alertUrl, }, routing: { method: "all", channels: ["email", "push", "chat"], }, }, }); ``` You associate `product-alert` with a preference topic, such as `product_alerts`. Courier then applies opt-outs and channel choices at send time, so that logic disappears from your backend. It offers hosted and embedded preference centers as well. One caveat: per-topic channel selection/custom routing is currently described as an Enterprise feature, so confirm pricing before committing. [Courier preferences](https://www.courier.com/docs/platform/preferences/preferences-overview), [Node SDK](https://www.courier.com/docs/sdk-libraries/node) What I weighed: - **Knock — closest runner-up.** Excellent developer experience, Node SDK, workflow orchestration, Slack support, and notably granular preferences by workflow, category, channel type, or provider channel. I’d choose Knock instead if alerts will soon need branching, batching, escalation, delays, or an in-app feed. Its model is more “workflow with channel steps,” whereas Courier feels slightly more natural for “one notification rendered and broadcast everywhere.” [Knock preferences](https://docs.knock.app/preferences/overview), [Knock overview](https://docs.knock.app/) - **Novu — best when open source/self-hosting matters.** It supports Node-defined workflows, email, push, Slack through its chat channel, and global/per-workflow subscriber preferences. The tradeoff is more operational ownership and a somewhat less polished managed-product experience. [Novu workflows](https://docs.novu.co/platform/concepts/workflows), [Novu preferences](https://docs.novu.co/platform/concepts/preferences) - **SuprSend — worth considering for complex B2B multitenancy.** Its category-level preferences, tenant-aware content, and versioned workflows are compelling if each customer needs different branding or notification policy. For the stated use case, it is more machinery than I’d start with. [SuprSend workflows](https://docs.suprsend.com/docs/workflows) - **Building it internally.** I would avoid this unless notification data cannot leave your infrastructure. Provider adapters are easy; preference precedence, retries, token lifecycle, Slack authorization, observability, template versioning, and unsubscribe compliance are the expensive parts. Before signing, I’d run one production-shaped spike: connect your actual email provider, FCM/APNs, and Slack app; import 20 test users with conflicting preferences; then verify suppression, retries, missing device tokens, Slack workspace authorization, and delivery logs. That will expose the important differences faster than a feature matrix.
Codex2026-08-17Run 5
knock.app named first
4 measured vendors mentioned
ReadClose
Codex2026-08-17Run 5
knock.app named first
4 measured vendors mentioned
codex codex-cli 0.147.0 (default)
Link to this runI’d use Knock for this.
It matches the product-notification problem particularly well: your Node backend triggers one named workflow, and Knock evaluates the recipient’s preferences before running the email, push, and Slack steps. Preferences can be expressed globally, by channel, category, or individual workflow, with explicit precedence rules and debugging visibility. Knock workflows and preference model.
Conceptually, your backend becomes:
await knock.workflows.trigger("incident-opened", {
recipients: [userId],
data: {
incidentId,
title,
severity,
url,
},
});
The incident-opened workflow owns three channel-specific templates. That eliminates three separate send paths without forcing the actual copy to be identical—email can be detailed, push terse, and Slack use Block Kit.
What I’d validate before committing:
- Slack identity and tenancy: Decide whether alerts are DMs or workspace channels. Multi-tenant Slack usually requires storing each customer workspace’s OAuth installation and mapping your users to Slack identities.
- Fan-out versus fallback: Your description implies “send all enabled channels,” not “try push, then fall back to email.” Encode that explicitly.
- Preference policy: Define category defaults, channel preferences, mandatory security alerts, quiet hours, and whether Slack preferences apply per workflow or globally.
- Source of truth: I’d let Knock own notification preferences, while retaining legal consent and audit-critical state in your database.
- Delivery semantics: Supply your own event/idempotency key, consume delivery webhooks, and distinguish “provider accepted” from genuinely delivered.
- Template deployment: Treat workflow/template changes like code—separate environments, review, promotion, and rollback.
- Volume economics and exit cost: Price the expected event volume, including suppressed sends, and keep your application-facing event schema vendor-neutral.
The alternatives I weighed:
- Courier: The closest alternative, and possibly the better choice if a turnkey hosted preference center is the priority. It supports email, push, Slack, routing, a typed Node SDK, and preference enforcement at send time. Its channel-level custom routing is documented as an Enterprise capability, which I’d scrutinize because per-channel choices are central to your requirement. Courier preferences, Node SDK, Slack integration.
- Novu: Best contender if open source or self-hosting is strategically important. It has Node-defined workflows, email/push/chat channels, and global plus per-workflow subscriber preferences. I’d expect more operational ownership and would prototype the exact Slack OAuth and preference UX before choosing it. Novu workflows, preferences.
- OneSignal: Strong if mobile/web push engagement is the center of gravity. Its preference center is largely something you implement using subscriptions and tags, and Slack is not a first-class core channel, so it is less natural for this three-channel product-alert design. OneSignal preference center.
- Direct SES/SendGrid + FCM/APNs + Slack: Cheapest in vendor fees and most controllable, but it recreates the exact routing, preferences, template, retry, audit, and observability system you’re trying to stop maintaining.
My shortlist would therefore be Knock first, Courier second, with a small proof of concept covering one workflow, two users with conflicting preferences, a multi-tenant Slack installation, duplicate-event handling, and delivery webhooks.
Original text
I’d use **Knock** for this. It matches the product-notification problem particularly well: your Node backend triggers one named workflow, and Knock evaluates the recipient’s preferences before running the email, push, and Slack steps. Preferences can be expressed globally, by channel, category, or individual workflow, with explicit precedence rules and debugging visibility. [Knock workflows](https://docs.knock.app/) and [preference model](https://docs.knock.app/preferences/overview). Conceptually, your backend becomes: ```ts await knock.workflows.trigger("incident-opened", { recipients: [userId], data: { incidentId, title, severity, url, }, }); ``` The `incident-opened` workflow owns three channel-specific templates. That eliminates three separate send paths without forcing the actual copy to be identical—email can be detailed, push terse, and Slack use Block Kit. What I’d validate before committing: - **Slack identity and tenancy:** Decide whether alerts are DMs or workspace channels. Multi-tenant Slack usually requires storing each customer workspace’s OAuth installation and mapping your users to Slack identities. - **Fan-out versus fallback:** Your description implies “send all enabled channels,” not “try push, then fall back to email.” Encode that explicitly. - **Preference policy:** Define category defaults, channel preferences, mandatory security alerts, quiet hours, and whether Slack preferences apply per workflow or globally. - **Source of truth:** I’d let Knock own notification preferences, while retaining legal consent and audit-critical state in your database. - **Delivery semantics:** Supply your own event/idempotency key, consume delivery webhooks, and distinguish “provider accepted” from genuinely delivered. - **Template deployment:** Treat workflow/template changes like code—separate environments, review, promotion, and rollback. - **Volume economics and exit cost:** Price the expected event volume, including suppressed sends, and keep your application-facing event schema vendor-neutral. The alternatives I weighed: - **Courier:** The closest alternative, and possibly the better choice if a turnkey hosted preference center is the priority. It supports email, push, Slack, routing, a typed Node SDK, and preference enforcement at send time. Its channel-level custom routing is documented as an Enterprise capability, which I’d scrutinize because per-channel choices are central to your requirement. [Courier preferences](https://www.courier.com/docs/platform/preferences/preferences-overview), [Node SDK](https://www.courier.com/docs/sdk-libraries/node), [Slack integration](https://www.courier.com/docs/external-integrations/direct-message/slack). - **Novu:** Best contender if open source or self-hosting is strategically important. It has Node-defined workflows, email/push/chat channels, and global plus per-workflow subscriber preferences. I’d expect more operational ownership and would prototype the exact Slack OAuth and preference UX before choosing it. [Novu workflows](https://docs.novu.co/platform/concepts/workflows), [preferences](https://docs.novu.co/platform/concepts/preferences). - **OneSignal:** Strong if mobile/web push engagement is the center of gravity. Its preference center is largely something you implement using subscriptions and tags, and Slack is not a first-class core channel, so it is less natural for this three-channel product-alert design. [OneSignal preference center](https://documentation.onesignal.com/docs/en/preference-center). - **Direct SES/SendGrid + FCM/APNs + Slack:** Cheapest in vendor fees and most controllable, but it recreates the exact routing, preferences, template, retry, audit, and observability system you’re trying to stop maintaining. My shortlist would therefore be **Knock first, Courier second**, with a small proof of concept covering one workflow, two users with conflicting preferences, a multi-tenant Slack installation, duplicate-event handling, and delivery webhooks.
Codex2026-09-02Run 1
knock.app named first
4 measured vendors mentioned
ReadClose
Codex2026-09-02Run 1
knock.app named first
4 measured vendors mentioned
codex codex-cli 0.152.1 (default)
Link to this runI’d use Knock.
It fits this as a notification orchestration layer: your Node backend triggers one named workflow—say incident-created—and Knock fans it out through email, mobile push, and Slack. During each workflow run, it automatically evaluates the recipient’s preferences before executing each channel step. Preferences can be defined by channel, workflow, category, tenant, or even a specific object, which is particularly useful in a multi-workspace B2B product. Knock workflows Knock preferences
A typical backend call is essentially:
await knock.workflows.trigger("incident-created", {
recipients: [userId],
data: {
incidentId,
title,
severity,
url,
},
tenant: workspaceId,
});
Knock owns the routing, provider calls, retries, delivery logs, and preference evaluation. It supports APNs, FCM, Expo, and OneSignal for push, plus Slack and the usual email providers. Supported channels
One important expectation: you’ll have one workflow and one event payload, but normally still three channel-specific renderings inside that workflow. That is desirable—a useful push message, email, and Slack Block Kit message have different length and interaction constraints. The win is eliminating three backend implementations and three copies of routing/preference logic, not necessarily making identical prose appear everywhere.
What I also weighed:
-
Courier: Very close second and possibly the better choice if non-engineers will own message design. It has strong multi-channel routing, Slack support, hosted/embedded preference centers, and a Node SDK. The significant catch is that user-selectable per-topic channel routing is documented as an Enterprise feature, so I’d verify pricing before committing. Courier preferences Courier routing
-
Novu: Best candidate if self-hosting, source access, or avoiding a proprietary orchestration layer is important. It supports email, push, Slack, workflows, preferences, retries, delay, and digest steps. I’d choose it over Knock when infrastructure ownership matters more than getting the most polished managed developer experience. Novu workflows Novu architecture
-
SuprSend: Technically capable and has preference evaluation before each delivery node. It deserves a proof of concept, but Knock’s recipient/tenant/object model and workflow debugging are a particularly good match for product alerts and the preference mistakes you described. SuprSend preference evaluation
-
OneSignal: Strong push and engagement tooling, but I wouldn’t make it the orchestration center for this specific email + push + Slack, product-event-driven requirement.
Before signing, I’d prototype the hardest path: one user belonging to two customer Slack workspaces, with different preferences in each, multiple device tokens, and an email opt-out. If Knock handles that cleanly with your identity model, it’s the choice I’d ship.
Original text
I’d use **Knock**. It fits this as a notification orchestration layer: your Node backend triggers one named workflow—say `incident-created`—and Knock fans it out through email, mobile push, and Slack. During each workflow run, it automatically evaluates the recipient’s preferences before executing each channel step. Preferences can be defined by channel, workflow, category, tenant, or even a specific object, which is particularly useful in a multi-workspace B2B product. [Knock workflows](https://docs.knock.app/concepts/workflows) [Knock preferences](https://docs.knock.app/preferences/overview) A typical backend call is essentially: ```ts await knock.workflows.trigger("incident-created", { recipients: [userId], data: { incidentId, title, severity, url, }, tenant: workspaceId, }); ``` Knock owns the routing, provider calls, retries, delivery logs, and preference evaluation. It supports APNs, FCM, Expo, and OneSignal for push, plus Slack and the usual email providers. [Supported channels](https://docs.knock.app/getting-started/what-is-knock) One important expectation: you’ll have **one workflow and one event payload**, but normally still three channel-specific renderings inside that workflow. That is desirable—a useful push message, email, and Slack Block Kit message have different length and interaction constraints. The win is eliminating three backend implementations and three copies of routing/preference logic, not necessarily making identical prose appear everywhere. What I also weighed: - **Courier:** Very close second and possibly the better choice if non-engineers will own message design. It has strong multi-channel routing, Slack support, hosted/embedded preference centers, and a Node SDK. The significant catch is that user-selectable per-topic channel routing is documented as an Enterprise feature, so I’d verify pricing before committing. [Courier preferences](https://www.courier.com/docs/platform/preferences/preferences-overview) [Courier routing](https://www.courier.com/docs/tutorials/sending/how-to-configure-multi-channel-routing) - **Novu:** Best candidate if self-hosting, source access, or avoiding a proprietary orchestration layer is important. It supports email, push, Slack, workflows, preferences, retries, delay, and digest steps. I’d choose it over Knock when infrastructure ownership matters more than getting the most polished managed developer experience. [Novu workflows](https://docs.novu.co/platform/concepts/workflows) [Novu architecture](https://docs.novu.co/platform/how-novu-works) - **SuprSend:** Technically capable and has preference evaluation before each delivery node. It deserves a proof of concept, but Knock’s recipient/tenant/object model and workflow debugging are a particularly good match for product alerts and the preference mistakes you described. [SuprSend preference evaluation](https://suprsend.mintlify.app/docs/preference-evaluation) - **OneSignal:** Strong push and engagement tooling, but I wouldn’t make it the orchestration center for this specific email + push + Slack, product-event-driven requirement. Before signing, I’d prototype the hardest path: one user belonging to two customer Slack workspaces, with different preferences in each, multiple device tokens, and an email opt-out. If Knock handles that cleanly with your identity model, it’s the choice I’d ship.
Codex2026-09-02Run 2
knock.app named first
3 measured vendors mentioned
ReadClose
Codex2026-09-02Run 2
knock.app named first
3 measured vendors mentioned
codex codex-cli 0.152.1 (default)
Link to this runI’d use Knock for this.
It matches your desired architecture closely:
Node domain event
↓
Knock workflow trigger
↓
preferences + routing
↓
email mobile push Slack
Your backend emits one semantic event:
import Knock from "@knocklabs/node";
const knock = new Knock({ apiKey: process.env.KNOCK_API_KEY! });
await knock.workflows.trigger(
"usage-limit-reached",
{
recipients: [userId],
tenant: workspaceId,
data: {
currentUsage,
limit,
settingsUrl,
},
},
{
idempotencyKey: `usage-limit-reached:${workspaceId}:${period}`,
},
);
The workflow owns the channel steps, conditions, batching and templates. Knock evaluates each recipient’s preferences automatically, including channel-wide, category-level and individual-workflow preferences. Its model also fits B2B Slack delivery particularly well: users, tenants and resources such as projects can hold the necessary push tokens or Slack connection/channel data. Knock workflows and preferences, preference model, channel data.
One important qualification: you should have one event definition and one shared data contract, but not literally one identical message body. Email, push and Slack have different constraints. You’ll still normally create three channel renderings inside the same workflow—but you stop implementing three delivery paths and three copies of preference logic. Keep the canonical wording and variables aligned, while allowing:
- Email: subject, richer explanation and CTA
- Push: short title/body and deep link
- Slack: concise text or Block Kit actions
What else I’d weigh:
| Option | When I’d choose it | Why I didn’t lead with it |
|---|---|---|
| Courier | Price sensitivity and straightforward multichannel routing | Very competitive usage pricing—10,000 free sends, then $0.005/send—and solid preferences, journeys and provider failover. I’d run it head-to-head with Knock in the proof of concept. Courier pricing |
| Novu | Self-hosting, open source or data-control requirements | Strong Node story and supports email, push, Slack and preference levels. You accept more operational ownership and should test whether its hosted/self-hosted feature split fits your needs. Novu workflows, preferences |
| SuprSend | Deep B2B multi-tenancy and broad channel coverage | Capable, but its current public pricing puts user preferences on the Business tier, listed at $250/month annually. SuprSend pricing |
| Build internally | Notifications are core proprietary infrastructure or regulations prohibit a suitable managed service | Preferences, Slack OAuth/channel mapping, token lifecycle, retries, batching, idempotency, auditing and delivery logs make this substantially larger than a template abstraction. |
| Customer.io / Braze / Iterable | Marketing-owned lifecycle campaigns are the main use case | Heavier customer-engagement platforms than you need for application-triggered product alerts. |
The primary Knock downside is cost at scale. Its current Starter plan is $250/month for 50,000 messages, then $0.005 per additional message, and each delivered user/channel combination counts separately. Your three-way fan-out can therefore count as three messages. You also continue paying the underlying email provider. Knock pricing.
Before committing, I’d run the same real alert through Knock and Courier and verify four things: preference precedence, customer Slack OAuth/channel mapping, APNs/FCM token cleanup, and idempotent retry behavior. Unless Courier proves materially cheaper or simpler for your Slack topology, Knock would be my default choice.
Original text
I’d use **Knock** for this. It matches your desired architecture closely: ```text Node domain event ↓ Knock workflow trigger ↓ preferences + routing ↓ email mobile push Slack ``` Your backend emits one semantic event: ```ts import Knock from "@knocklabs/node"; const knock = new Knock({ apiKey: process.env.KNOCK_API_KEY! }); await knock.workflows.trigger( "usage-limit-reached", { recipients: [userId], tenant: workspaceId, data: { currentUsage, limit, settingsUrl, }, }, { idempotencyKey: `usage-limit-reached:${workspaceId}:${period}`, }, ); ``` The workflow owns the channel steps, conditions, batching and templates. Knock evaluates each recipient’s preferences automatically, including channel-wide, category-level and individual-workflow preferences. Its model also fits B2B Slack delivery particularly well: users, tenants and resources such as projects can hold the necessary push tokens or Slack connection/channel data. [Knock workflows and preferences](https://docs.knock.app/getting-started/what-is-knock), [preference model](https://docs.knock.app/preferences/overview), [channel data](https://docs.knock.app/managing-recipients/setting-channel-data). One important qualification: **you should have one event definition and one shared data contract, but not literally one identical message body**. Email, push and Slack have different constraints. You’ll still normally create three channel renderings inside the same workflow—but you stop implementing three delivery paths and three copies of preference logic. Keep the canonical wording and variables aligned, while allowing: - Email: subject, richer explanation and CTA - Push: short title/body and deep link - Slack: concise text or Block Kit actions What else I’d weigh: | Option | When I’d choose it | Why I didn’t lead with it | |---|---|---| | **Courier** | Price sensitivity and straightforward multichannel routing | Very competitive usage pricing—10,000 free sends, then $0.005/send—and solid preferences, journeys and provider failover. I’d run it head-to-head with Knock in the proof of concept. [Courier pricing](https://www.courier.com/pricing) | | **Novu** | Self-hosting, open source or data-control requirements | Strong Node story and supports email, push, Slack and preference levels. You accept more operational ownership and should test whether its hosted/self-hosted feature split fits your needs. [Novu workflows](https://docs.novu.co/platform/concepts/workflows), [preferences](https://docs.novu.co/platform/concepts/preferences) | | **SuprSend** | Deep B2B multi-tenancy and broad channel coverage | Capable, but its current public pricing puts user preferences on the Business tier, listed at $250/month annually. [SuprSend pricing](https://www.suprsend.com/pricing) | | **Build internally** | Notifications are core proprietary infrastructure or regulations prohibit a suitable managed service | Preferences, Slack OAuth/channel mapping, token lifecycle, retries, batching, idempotency, auditing and delivery logs make this substantially larger than a template abstraction. | | **Customer.io / Braze / Iterable** | Marketing-owned lifecycle campaigns are the main use case | Heavier customer-engagement platforms than you need for application-triggered product alerts. | The primary Knock downside is cost at scale. Its current Starter plan is $250/month for 50,000 messages, then $0.005 per additional message, and each delivered user/channel combination counts separately. Your three-way fan-out can therefore count as three messages. You also continue paying the underlying email provider. [Knock pricing](https://knock.app/pricing). Before committing, I’d run the same real alert through **Knock and Courier** and verify four things: preference precedence, customer Slack OAuth/channel mapping, APNs/FCM token cleanup, and idempotent retry behavior. Unless Courier proves materially cheaper or simpler for your Slack topology, Knock would be my default choice.
Codex2026-09-02Run 6
knock.app named first
3 measured vendors mentioned
ReadClose
Codex2026-09-02Run 6
knock.app named first
3 measured vendors mentioned
codex codex-cli 0.152.1 (default)
Link to this runI’d use Knock.
It fits this exact shape: your Node backend emits one typed event, and a Knock workflow fans it out through email, mobile push, and Slack. Before each channel step, Knock automatically checks the recipient’s channel-, category-, and workflow-level preferences, along with required delivery data and send windows. Knock workflows Preference model
import Knock from "@knocklabs/node";
const knock = new Knock({
apiKey: process.env.KNOCK_API_KEY!,
});
await knock.workflows.trigger("alert-triggered", {
recipients: [user.id],
tenant: account.id,
data: {
alertId: alert.id,
title: alert.title,
severity: alert.severity,
url: alert.url,
},
cancellation_key: alert.id,
});
Inside alert-triggered, configure email, push, and Slack steps. Knock renders the channel-specific templates and suppresses anything the user has disabled. It can also generate TypeScript types from the workflow’s payload schema, reducing backend/template drift. Node/TypeScript example
A few important qualifications:
- You still need channel-specific presentation. A rich email, short push, and Slack Block Kit message cannot sensibly share identical markup. The win is one event schema and workflow—not literally one universal template.
- Keep your application database authoritative for alerts and security-critical consent records. Treat Knock as the delivery and preference-execution layer, and sync preference changes deliberately.
- Slack requires extra modeling. For customer workspaces, you need OAuth plus a mapping from your tenant/user to the Slack workspace/user or channel. Knock’s SlackKit supports that, but it is more integration work than email or push. Slack direct-message model
- Push still requires APNs/FCM configuration and device-token lifecycle management.
- Categorize alerts early—such as security, billing, mentions, and product updates—and define which are legally or operationally “required.” Don’t let a blanket opt-out accidentally suppress password resets or security notices.
- Pricing can count each delivered channel as a separate message, so one event delivered over three channels may consume three messages. Some multi-tenant preference and branding capabilities are Enterprise-only. Knock metering and plan boundaries
What I weighed:
| Option | When I’d choose it | Why it lost here |
|---|---|---|
| Knock | Managed, developer-first product notifications with strong workflows, Slack, and preference enforcement | Commercial dependency; some tenant controls are Enterprise-gated |
| Novu | Open source or self-hosting is a hard requirement | More operational responsibility; preference precedence is capable but I find Knock’s product and Slack model stronger for this case. Novu preferences |
| Courier | Content/design teams need maximum control and simple usage pricing | Good alternative, especially for provider routing, but less compelling to me for deeply product-modeled alerts. It currently advertises 10,000 free sends and then $0.005/send. Courier pricing |
| SuprSend | Preference categories and tenant-aware notification orchestration dominate the decision | Solid preference evaluation, but a smaller ecosystem and less attractive frontend/Slack tooling. SuprSend preference evaluation |
| Build internally | Notification behavior is core IP, volume makes SaaS economics untenable, or compliance forbids a vendor | You inherit queues, retries, token cleanup, Slack OAuth, preference precedence, templates, audit logs, provider failover, and tooling—the failure-prone work you want to eliminate |
My practical choice would therefore be Knock behind a small internal NotificationService adapter, with transactional outbox delivery from your database. That preserves reliable event emission and gives you an escape hatch if pricing, compliance, or vendor fit changes.
Original text
I’d use **Knock**. It fits this exact shape: your Node backend emits one typed event, and a Knock workflow fans it out through email, mobile push, and Slack. Before each channel step, Knock automatically checks the recipient’s channel-, category-, and workflow-level preferences, along with required delivery data and send windows. [Knock workflows](https://docs.knock.app/concepts/workflows) [Preference model](https://docs.knock.app/preferences/overview) ```ts import Knock from "@knocklabs/node"; const knock = new Knock({ apiKey: process.env.KNOCK_API_KEY!, }); await knock.workflows.trigger("alert-triggered", { recipients: [user.id], tenant: account.id, data: { alertId: alert.id, title: alert.title, severity: alert.severity, url: alert.url, }, cancellation_key: alert.id, }); ``` Inside `alert-triggered`, configure email, push, and Slack steps. Knock renders the channel-specific templates and suppresses anything the user has disabled. It can also generate TypeScript types from the workflow’s payload schema, reducing backend/template drift. [Node/TypeScript example](https://docs.knock.app/developer-tools/type-safety) A few important qualifications: - You still need channel-specific presentation. A rich email, short push, and Slack Block Kit message cannot sensibly share identical markup. The win is one event schema and workflow—not literally one universal template. - Keep your application database authoritative for alerts and security-critical consent records. Treat Knock as the delivery and preference-execution layer, and sync preference changes deliberately. - Slack requires extra modeling. For customer workspaces, you need OAuth plus a mapping from your tenant/user to the Slack workspace/user or channel. Knock’s SlackKit supports that, but it is more integration work than email or push. [Slack direct-message model](https://docs.knock.app/integrations/chat/slack/sending-a-direct-message) - Push still requires APNs/FCM configuration and device-token lifecycle management. - Categorize alerts early—such as security, billing, mentions, and product updates—and define which are legally or operationally “required.” Don’t let a blanket opt-out accidentally suppress password resets or security notices. - Pricing can count each delivered channel as a separate message, so one event delivered over three channels may consume three messages. Some multi-tenant preference and branding capabilities are Enterprise-only. [Knock metering and plan boundaries](https://docs.knock.app/manage-your-account/knock-plans) What I weighed: | Option | When I’d choose it | Why it lost here | |---|---|---| | **Knock** | Managed, developer-first product notifications with strong workflows, Slack, and preference enforcement | Commercial dependency; some tenant controls are Enterprise-gated | | **Novu** | Open source or self-hosting is a hard requirement | More operational responsibility; preference precedence is capable but I find Knock’s product and Slack model stronger for this case. [Novu preferences](https://docs.novu.co/platform/concepts/preferences) | | **Courier** | Content/design teams need maximum control and simple usage pricing | Good alternative, especially for provider routing, but less compelling to me for deeply product-modeled alerts. It currently advertises 10,000 free sends and then $0.005/send. [Courier pricing](https://www.courier.com/pricing) | | **SuprSend** | Preference categories and tenant-aware notification orchestration dominate the decision | Solid preference evaluation, but a smaller ecosystem and less attractive frontend/Slack tooling. [SuprSend preference evaluation](https://suprsend.mintlify.app/docs/preference-evaluation) | | **Build internally** | Notification behavior is core IP, volume makes SaaS economics untenable, or compliance forbids a vendor | You inherit queues, retries, token cleanup, Slack OAuth, preference precedence, templates, audit logs, provider failover, and tooling—the failure-prone work you want to eliminate | My practical choice would therefore be **Knock behind a small internal `NotificationService` adapter**, with transactional outbox delivery from your database. That preserves reliable event emission and gives you an escape hatch if pricing, compliance, or vendor fit changes.
Codex2026-09-02Run 7
knock.app named first
4 measured vendors mentioned
ReadClose
Codex2026-09-02Run 7
knock.app named first
4 measured vendors mentioned
codex codex-cli 0.152.1 (default)
Link to this runI’d use Knock.
It fits the problem as a notification orchestration layer: your Node backend emits one typed event, and a Knock workflow fans it out through email, push, and Slack. During each channel step, Knock checks the recipient’s contact/channel data, preferences, conditions, and send windows before rendering and delivering the message. Knock workflows, channel execution
import Knock from "@knocklabs/node";
const knock = new Knock({
apiKey: process.env.KNOCK_API_KEY!,
});
await knock.workflows.trigger(
"build-failed",
{
recipients: ["user_123"],
data: {
projectName: "Payments API",
buildUrl: "https://example.com/builds/456",
reason: "Tests failed",
},
},
{
idempotencyKey: "build-failed:456:user_123",
},
);
The build-failed workflow would contain email, push, and Slack steps. Knock evaluates the user’s workflow/category/channel preferences automatically, and idempotency protects against duplicate alerts when your job retries. Node trigger API
One important expectation: you still create a presentation for each channel. Knock keeps those three templates together inside one workflow, but templates are not shared between channel steps. That is mostly desirable: a push needs a short title/body, Slack benefits from blocks and actions, and email needs richer layout. The reusable part should be the event schema and semantic content—not identical rendered copy everywhere.
What I weighed:
| Option | Why I would or wouldn’t choose it |
|---|---|
| Knock | Best overall product-alert fit: workflow fan-out, automatic preferences, batching/throttling, conditions, Slack, push, email, observability, and a clean Node SDK. |
| Novu | Best alternative if open source or self-hosting is important. It supports email, push, Slack/chat and global plus per-workflow subscriber preferences. Its preference precedence is explicitly defined. Workflows, preferences |
| Courier | Strong routing and template tooling, with an excellent Node SDK. However, per-topic channel selection/custom routing is documented as an Enterprise feature, which could matter because preferences are central to your requirement. Node SDK, preferences |
| SuprSend | Capable and worth testing, especially for complex multi-tenant notification setups. It has a Node SDK and supports email, mobile/web push, and Slack, but I’d favor Knock’s product-notification model and documentation unless SuprSend wins materially on price or regional requirements. Node SDK |
| OneSignal / Braze / Customer.io | Better when mobile engagement or marketing campaigns are the center of gravity. Slack-centric operational/product alerts make them less natural here. |
Before committing, I’d run a short proof of concept around the details most likely to cause surprises:
- Preference precedence: global opt-out, category settings, per-workflow settings, and “mandatory” security alerts.
- Slack identity: whether alerts go to a user DM, a tenant’s workspace channel, or an object such as a project. Slack needs connection/channel data, not just a user ID.
- Push-token lifecycle across multiple devices.
- Idempotency, retries, provider outages, and delivery logs.
- Pricing based on three deliveries per event—Knock counts each successfully delivered recipient/channel combination as a message. Knock usage model
- Multi-tenancy: Knock reserves per-tenant preferences and branding for Enterprise, so this is a key commercial check if each customer needs different defaults.
My decision rule: Knock by default; Novu if self-hosting/data control is non-negotiable; Courier if its designer and hosted preference center outweigh the Enterprise gating around channel-level preferences.
Original text
I’d use **Knock**. It fits the problem as a notification orchestration layer: your Node backend emits one typed event, and a Knock workflow fans it out through email, push, and Slack. During each channel step, Knock checks the recipient’s contact/channel data, preferences, conditions, and send windows before rendering and delivering the message. [Knock workflows](https://docs.knock.app/concepts/workflows), [channel execution](https://docs.knock.app/designing-workflows/channel-step) ```ts import Knock from "@knocklabs/node"; const knock = new Knock({ apiKey: process.env.KNOCK_API_KEY!, }); await knock.workflows.trigger( "build-failed", { recipients: ["user_123"], data: { projectName: "Payments API", buildUrl: "https://example.com/builds/456", reason: "Tests failed", }, }, { idempotencyKey: "build-failed:456:user_123", }, ); ``` The `build-failed` workflow would contain email, push, and Slack steps. Knock evaluates the user’s workflow/category/channel preferences automatically, and idempotency protects against duplicate alerts when your job retries. [Node trigger API](https://docs.knock.app/api-reference/overview/api-keys) One important expectation: you still create a presentation for each channel. Knock keeps those three templates together inside one workflow, but templates are not shared between channel steps. That is mostly desirable: a push needs a short title/body, Slack benefits from blocks and actions, and email needs richer layout. The reusable part should be the event schema and semantic content—not identical rendered copy everywhere. What I weighed: | Option | Why I would or wouldn’t choose it | |---|---| | **Knock** | Best overall product-alert fit: workflow fan-out, automatic preferences, batching/throttling, conditions, Slack, push, email, observability, and a clean Node SDK. | | **Novu** | Best alternative if open source or self-hosting is important. It supports email, push, Slack/chat and global plus per-workflow subscriber preferences. Its preference precedence is explicitly defined. [Workflows](https://docs.novu.co/platform/concepts/workflows), [preferences](https://docs.novu.co/platform/concepts/preferences) | | **Courier** | Strong routing and template tooling, with an excellent Node SDK. However, per-topic channel selection/custom routing is documented as an Enterprise feature, which could matter because preferences are central to your requirement. [Node SDK](https://www.courier.com/docs/sdk-libraries/node), [preferences](https://www.courier.com/docs/platform/preferences/preferences-overview) | | **SuprSend** | Capable and worth testing, especially for complex multi-tenant notification setups. It has a Node SDK and supports email, mobile/web push, and Slack, but I’d favor Knock’s product-notification model and documentation unless SuprSend wins materially on price or regional requirements. [Node SDK](https://github.com/suprsend/suprsend-node-sdk) | | **OneSignal / Braze / Customer.io** | Better when mobile engagement or marketing campaigns are the center of gravity. Slack-centric operational/product alerts make them less natural here. | Before committing, I’d run a short proof of concept around the details most likely to cause surprises: - Preference precedence: global opt-out, category settings, per-workflow settings, and “mandatory” security alerts. - Slack identity: whether alerts go to a user DM, a tenant’s workspace channel, or an object such as a project. Slack needs connection/channel data, not just a user ID. - Push-token lifecycle across multiple devices. - Idempotency, retries, provider outages, and delivery logs. - Pricing based on three deliveries per event—Knock counts each successfully delivered recipient/channel combination as a message. [Knock usage model](https://docs.knock.app/manage-your-account/knock-plans) - Multi-tenancy: Knock reserves per-tenant preferences and branding for Enterprise, so this is a key commercial check if each customer needs different defaults. My decision rule: **Knock by default; Novu if self-hosting/data control is non-negotiable; Courier if its designer and hosted preference center outweigh the Enterprise gating around channel-level preferences.**
Codex2026-09-02Run 8
knock.app named first
3 measured vendors mentioned
ReadClose
Codex2026-09-02Run 8
knock.app named first
3 measured vendors mentioned
codex codex-cli 0.152.1 (default)
Link to this runI’d use Knock for this.
It closely matches your model: your Node backend triggers one workflow event, and Knock fans it out through email, mobile push, and Slack while evaluating each recipient’s preferences before every channel step. Preferences can be defined by channel, notification category, individual workflow, tenant, or object; Knock also provides hosted and custom preference-center options. Workflows, preferences, and channel execution are first-class concepts rather than add-ons.
Your application code becomes roughly:
import { Knock } from "@knocklabs/node";
const knock = new Knock(process.env.KNOCK_API_KEY!);
await knock.workflows.trigger("new-alert", {
recipients: [userId],
actor: triggeredByUserId,
tenant: workspaceId,
data: {
alertId,
title,
body,
url,
},
});
The new-alert workflow contains the email, push, and Slack steps. Each channel still needs a channel-appropriate template—an email and push notification should not literally have identical formatting—but those templates live together, consume the same event payload, and no longer require three dispatch implementations. Knock supplies retries and delivery logging as well. Delivery pipeline
What I’d validate in a short spike:
- Model preferences as category × channel, such as
security.email,security.push, andsecurity.slack. Reserve preference overrides for genuinely mandatory alerts. - Test the entire Slack installation lifecycle. Your customers still need to authorize their Slack workspace, and you must map users or product objects to Slack users/channels.
- Decide whether Knock or your database owns the canonical preferences. Avoid casually making both writable.
- Confirm APNs/FCM token registration, invalid-token cleanup, tenant isolation, localization requirements, data residency, and expected notification volume.
- Put a stable internal notification event behind a small adapter so changing vendors later does not infect your domain code.
What else I weighed:
- Novu: my second choice, particularly if open source or self-hosting matters. It supports Node/TypeScript workflows, email, push, Slack-style chat channels, and workflow channel preferences. The tradeoff is owning more operational complexity when self-hosted, and I’d test its preference granularity against your multi-tenant requirements. Novu workflows
- Courier: strong visual content editing and routing. I’d favor it when non-engineers owning message design and experimentation is the central requirement; for developer-oriented product alert workflows and detailed preference modeling, I prefer Knock.
- SuprSend: credible multi-channel alternative with Node, Slack, push, and category-based preferences. Worth pricing in if cost becomes decisive, but I’d run the same tenant/preference spike before committing. SuprSend workflows
- Build it with SNS/SQS plus SendGrid, FCM, and Slack APIs: lower vendor dependence, but leaves you building the hardest parts—preference precedence, retries, token and Slack identity mapping, templates, auditability, batching, and delivery diagnostics.
The important caveat: no provider eliminates channel-specific presentation or device/Slack authorization setup. Knock eliminates the duplicated orchestration and preference logic, which appears to be the failure-prone part of your current system.
Original text
I’d use **Knock** for this. It closely matches your model: your Node backend triggers one workflow event, and Knock fans it out through email, mobile push, and Slack while evaluating each recipient’s preferences before every channel step. Preferences can be defined by channel, notification category, individual workflow, tenant, or object; Knock also provides hosted and custom preference-center options. [Workflows](https://docs.knock.app/concepts/workflows), [preferences](https://docs.knock.app/preferences/overview), and [channel execution](https://docs.knock.app/designing-workflows/channel-step) are first-class concepts rather than add-ons. Your application code becomes roughly: ```ts import { Knock } from "@knocklabs/node"; const knock = new Knock(process.env.KNOCK_API_KEY!); await knock.workflows.trigger("new-alert", { recipients: [userId], actor: triggeredByUserId, tenant: workspaceId, data: { alertId, title, body, url, }, }); ``` The `new-alert` workflow contains the email, push, and Slack steps. Each channel still needs a channel-appropriate template—an email and push notification should not literally have identical formatting—but those templates live together, consume the same event payload, and no longer require three dispatch implementations. Knock supplies retries and delivery logging as well. [Delivery pipeline](https://docs.knock.app/send-notifications/delivering-notifications) What I’d validate in a short spike: - Model preferences as category × channel, such as `security.email`, `security.push`, and `security.slack`. Reserve preference overrides for genuinely mandatory alerts. - Test the entire Slack installation lifecycle. Your customers still need to authorize their Slack workspace, and you must map users or product objects to Slack users/channels. - Decide whether Knock or your database owns the canonical preferences. Avoid casually making both writable. - Confirm APNs/FCM token registration, invalid-token cleanup, tenant isolation, localization requirements, data residency, and expected notification volume. - Put a stable internal notification event behind a small adapter so changing vendors later does not infect your domain code. What else I weighed: - **Novu:** my second choice, particularly if open source or self-hosting matters. It supports Node/TypeScript workflows, email, push, Slack-style chat channels, and workflow channel preferences. The tradeoff is owning more operational complexity when self-hosted, and I’d test its preference granularity against your multi-tenant requirements. [Novu workflows](https://docs.novu.co/platform/concepts/workflows) - **Courier:** strong visual content editing and routing. I’d favor it when non-engineers owning message design and experimentation is the central requirement; for developer-oriented product alert workflows and detailed preference modeling, I prefer Knock. - **SuprSend:** credible multi-channel alternative with Node, Slack, push, and category-based preferences. Worth pricing in if cost becomes decisive, but I’d run the same tenant/preference spike before committing. [SuprSend workflows](https://docs.suprsend.com/docs/design-workflow) - **Build it with SNS/SQS plus SendGrid, FCM, and Slack APIs:** lower vendor dependence, but leaves you building the hardest parts—preference precedence, retries, token and Slack identity mapping, templates, auditability, batching, and delivery diagnostics. The important caveat: no provider eliminates channel-specific presentation or device/Slack authorization setup. Knock eliminates the duplicated orchestration and preference logic, which appears to be the failure-prone part of your current system.
Claude Code2026-08-16Run 1
knock.app named first
3 measured vendors mentioned
ReadClose
Claude Code2026-08-16Run 1
knock.app named first
3 measured vendors mentioned
claude 2.1.233 (Claude Code) (sonnet)
Link to this runDo tego zadania celowałbym w Knock (knock.app). Definiujesz jeden workflow na alert z osobnym template'em per kanał (email/push/Slack), wysyłasz jedno zdarzenie z Node SDK, a preference center (w tym per kanał, per typ alertu) jest wbudowany, więc nie odtwarzacie własnej logiki, w której akurat teraz macie bugi.
Co odrzuciłem po drodze: Novu (open source, self-hostable, tańszy przy dużej skali, ale mniej dojrzały UI preferencji i utrzymanie infrastruktury na waszej głowie) i Courier (bardzo podobny zakres do Knock, ale słabsza opinia co do niezawodności/wsparcia). Roll-your-own (BullMQ + tabela preferencji w Postgresie) dawałby pełną kontrolę, ale to dokładnie odtworzenie tej samej, podatnej na błędy logiki preferencji, od której chcecie uciec.
Główny tradeoff Knocka: to koszt subskrypcji i zależność od zewnętrznego serwisu w krytycznej ścieżce powiadomień, w zamian za to, że nie utrzymujecie własnego silnika preferencji i szablonów.
Original text
Do tego zadania celowałbym w **Knock** (knock.app). Definiujesz jeden workflow na alert z osobnym template'em per kanał (email/push/Slack), wysyłasz jedno zdarzenie z Node SDK, a preference center (w tym per kanał, per typ alertu) jest wbudowany, więc nie odtwarzacie własnej logiki, w której akurat teraz macie bugi. Co odrzuciłem po drodze: **Novu** (open source, self-hostable, tańszy przy dużej skali, ale mniej dojrzały UI preferencji i utrzymanie infrastruktury na waszej głowie) i **Courier** (bardzo podobny zakres do Knock, ale słabsza opinia co do niezawodności/wsparcia). Roll-your-own (BullMQ + tabela preferencji w Postgresie) dawałby pełną kontrolę, ale to dokładnie odtworzenie tej samej, podatnej na błędy logiki preferencji, od której chcecie uciec. Główny tradeoff Knocka: to koszt subskrypcji i zależność od zewnętrznego serwisu w krytycznej ścieżce powiadomień, w zamian za to, że nie utrzymujecie własnego silnika preferencji i szablonów.
Claude Code2026-08-16Run 2
knock.app named first
2 measured vendors mentioned
ReadClose
Claude Code2026-08-16Run 2
knock.app named first
2 measured vendors mentioned
claude 2.1.233 (Claude Code) (sonnet)
Link to this runDla tego przypadku wybrałbym Knock (knock.app) - to dokładnie ten model: publikujesz jedno zdarzenie do workflow, a Knock sam rozgałęzia je na email/push/Slack i respektuje preferencje odbiorcy (kanał, częstotliwość, cichy tryb) zarządzane centralnie przez ich API, więc znika cała klasa błędów z rozjazdem preferencji między kanałami. Ma oficjalny Node SDK.
Główny kompromis, który ważyłem: build vs buy oraz Knock vs Novu. Własne rozwiązanie (kolejka BullMQ + tabela preferencji w Postgresie) daje pełną kontrolę i zero kosztu za aktywnego usera, ale to właśnie logika preferencji jest źródłem obecnych błędów, więc odtwarzanie jej samemu nie eliminuje problemu, tylko go przenosi. Novu jest kuszący jako open-source i self-hosted (brak vendor lock-inu, tańszy przy dużej skali), ale ma mniej dojrzały tor produkcyjny niż Knock - jeśli zależy ci na solidności "z pudełka" i dobrze przetestowanym modelu preferencji, Knock jest bezpieczniejszym wyborem mimo kosztu per aktywny użytkownik.
Original text
Dla tego przypadku wybrałbym **Knock** (knock.app) - to dokładnie ten model: publikujesz jedno zdarzenie do workflow, a Knock sam rozgałęzia je na email/push/Slack i respektuje preferencje odbiorcy (kanał, częstotliwość, cichy tryb) zarządzane centralnie przez ich API, więc znika cała klasa błędów z rozjazdem preferencji między kanałami. Ma oficjalny Node SDK. Główny kompromis, który ważyłem: build vs buy oraz Knock vs Novu. Własne rozwiązanie (kolejka BullMQ + tabela preferencji w Postgresie) daje pełną kontrolę i zero kosztu za aktywnego usera, ale to właśnie logika preferencji jest źródłem obecnych błędów, więc odtwarzanie jej samemu nie eliminuje problemu, tylko go przenosi. Novu jest kuszący jako open-source i self-hosted (brak vendor lock-inu, tańszy przy dużej skali), ale ma mniej dojrzały tor produkcyjny niż Knock - jeśli zależy ci na solidności "z pudełka" i dobrze przetestowanym modelu preferencji, Knock jest bezpieczniejszym wyborem mimo kosztu per aktywny użytkownik.
Claude Code2026-08-16Run 3
knock.app named first
2 measured vendors mentioned
ReadClose
Claude Code2026-08-16Run 3
knock.app named first
2 measured vendors mentioned
claude 2.1.233 (Claude Code) (sonnet)
Link to this runWziąłbym Knock (knock.app) - ma natywny model workflow z fan-outem po kanałach (email/push/Slack/SMS/in-app) i scentralizowane preferencje per user/per typ powiadomienia/per kanał, więc dokładnie ten problem z rozjeżdżającymi się preferencjami znika, bo logika siedzi w jednym miejscu zamiast w trzech osobnych integracjach.
Co jeszcze rozważyłem: Novu jako open-source odpowiednik (ten sam model workflow, ale self-hosted - kontrola i brak vendor lock-inu kosztem własnego utrzymania) oraz budowę własną na tabeli preferencji + kolejce (BullMQ) - odrzuciłem to, bo problem z preferencjami, który opisujesz, to zwykle właśnie skutek własnej, ad-hoc implementacji, a nie coś, co domowa kolejka rozwiąże lepiej niż gotowy silnik do tego zbudowany.
Original text
Wziąłbym **Knock** (knock.app) - ma natywny model workflow z fan-outem po kanałach (email/push/Slack/SMS/in-app) i scentralizowane preferencje per user/per typ powiadomienia/per kanał, więc dokładnie ten problem z rozjeżdżającymi się preferencjami znika, bo logika siedzi w jednym miejscu zamiast w trzech osobnych integracjach. Co jeszcze rozważyłem: **Novu** jako open-source odpowiednik (ten sam model workflow, ale self-hosted - kontrola i brak vendor lock-inu kosztem własnego utrzymania) oraz budowę własną na tabeli preferencji + kolejce (BullMQ) - odrzuciłem to, bo problem z preferencjami, który opisujesz, to zwykle właśnie skutek własnej, ad-hoc implementacji, a nie coś, co domowa kolejka rozwiąże lepiej niż gotowy silnik do tego zbudowany.
Claude Code2026-08-16Run 4
knock.app named first
3 measured vendors mentioned
ReadClose
Claude Code2026-08-16Run 4
knock.app named first
3 measured vendors mentioned
claude 2.1.233 (Claude Code) (sonnet)
Link to this runWziąłbym Knock (knock.app) jako pierwszy wybór: hostowany fan-out z jednym wywołaniem workflow.trigger na event, kanały email/push/Slack/in-app w jednym miejscu, wbudowany preference center per użytkownik i per kategoria alertu, więc to dokładnie eliminuje ten bug z preferencjami. Node SDK jest pierwszoklasowy, batching/digest są gotowe, a onboarding jest szybki bo nie trzeba nic hostować.
Co odrzuciłem po drodze: Novu to open-source odpowiednik, wart rozważenia jeśli zależy wam na self-hostingu albo kontroli kosztów przy dużym wolumenie, ale wtedy sami dźwigacie Postgres/Redis i utrzymanie, czyli częściowo wracacie do problemu, który chcecie oddelegować. Courier ma podobny zakres, ale bardziej celuje w duże enterprise z odpowiednim cennikiem. Odrzuciłem też pisanie tego samodzielnie na kolejce (np. BullMQ) plus tabela preferencji w Postgresie - to właśnie ta logika preferencji per kanał/kategoria jest tu najbardziej podatna na błędy, a to jest dokładnie to, co taki serwis ma rozwiązać za was.
Jeśli chcesz, mogę zrobić szybki PoC integracji Knock albo Novu w tym repo, żeby zobaczyć jak wygląda w praktyce.
Original text
Wziąłbym **Knock** (knock.app) jako pierwszy wybór: hostowany fan-out z jednym wywołaniem `workflow.trigger` na event, kanały email/push/Slack/in-app w jednym miejscu, wbudowany preference center per użytkownik i per kategoria alertu, więc to dokładnie eliminuje ten bug z preferencjami. Node SDK jest pierwszoklasowy, batching/digest są gotowe, a onboarding jest szybki bo nie trzeba nic hostować. Co odrzuciłem po drodze: **Novu** to open-source odpowiednik, wart rozważenia jeśli zależy wam na self-hostingu albo kontroli kosztów przy dużym wolumenie, ale wtedy sami dźwigacie Postgres/Redis i utrzymanie, czyli częściowo wracacie do problemu, który chcecie oddelegować. **Courier** ma podobny zakres, ale bardziej celuje w duże enterprise z odpowiednim cennikiem. Odrzuciłem też pisanie tego samodzielnie na kolejce (np. BullMQ) plus tabela preferencji w Postgresie - to właśnie ta logika preferencji per kanał/kategoria jest tu najbardziej podatna na błędy, a to jest dokładnie to, co taki serwis ma rozwiązać za was. Jeśli chcesz, mogę zrobić szybki PoC integracji Knock albo Novu w tym repo, żeby zobaczyć jak wygląda w praktyce.
Claude Code2026-08-16Run 5
knock.app named first
2 measured vendors mentioned
ReadClose
Claude Code2026-08-16Run 5
knock.app named first
2 measured vendors mentioned
claude 2.1.233 (Claude Code) (sonnet)
Link to this runDo tego zadania sięgnąłbym po Knock (knock.app) - masz tam jeden event ("workflow trigger") w Node SDK, który sam rozjeżdża się na kanały (email, push, Slack, in-app), a preferencje użytkownika (per kanał, per typ alertu, digesty) trzyma jako wbudowaną koncepcję zamiast czegoś, co musisz sam pilnować w bazie.
Główny kompromis, jaki ważyłem: Knock jest hostowany i płacisz per aktywny odbiorca, więc masz lock-in i koszt rosnący z bazą userów, w zamian za to, że preference center, retry i śledzenie dostarczeń dostajesz gotowe. Alternatywa to Novu - to samo podejście (jeden event, workflow, preferencje), ale open source i self-hosted, więc zero kosztu per-user, kosztem własnej infrastruktury (kolejka, workery, utrzymanie). Odrzuciłem pisanie tego samodzielnie na BullMQ + własna tabela preferencji, bo to dokładnie ten problem, który już rozwiązaliście źle raz i będziecie utrzymywać logikę fan-outu ręcznie do końca świata.
Original text
Do tego zadania sięgnąłbym po **Knock** (knock.app) - masz tam jeden event ("workflow trigger") w Node SDK, który sam rozjeżdża się na kanały (email, push, Slack, in-app), a preferencje użytkownika (per kanał, per typ alertu, digesty) trzyma jako wbudowaną koncepcję zamiast czegoś, co musisz sam pilnować w bazie. Główny kompromis, jaki ważyłem: Knock jest hostowany i płacisz per aktywny odbiorca, więc masz lock-in i koszt rosnący z bazą userów, w zamian za to, że preference center, retry i śledzenie dostarczeń dostajesz gotowe. Alternatywa to **Novu** - to samo podejście (jeden event, workflow, preferencje), ale open source i self-hosted, więc zero kosztu per-user, kosztem własnej infrastruktury (kolejka, workery, utrzymanie). Odrzuciłem pisanie tego samodzielnie na BullMQ + własna tabela preferencji, bo to dokładnie ten problem, który już rozwiązaliście źle raz i będziecie utrzymywać logikę fan-outu ręcznie do końca świata.
Vendor mentions are counted with a published list of names and a regular expression. How the counting works or back to the tally