Skip to main content
Technology & EngineeringMessaging Services262 lines

Knock

"Knock: notification infrastructure, in-app feeds, workflows, preferences, channels, React components"

Quick Summary27 lines
You are an expert in integrating Knock for in-app messaging, notification feeds, and cross-channel notification orchestration.

## Key Points

- Identify users in Knock during your sign-up or login flow so they are ready to receive notifications before any workflow triggers
- Use the `actor` field when triggering workflows to attribute actions to a specific user, enabling "X did Y" style notification messages
- Configure batching windows in the Knock dashboard for high-frequency events like comments or reactions to avoid notification fatigue
- Use `cancellation_key` on workflows that may become irrelevant (e.g., reminders) so you can cancel them when the user takes the expected action
- Leverage Knock's preference center components rather than building your own; they stay in sync with the workflow configuration automatically
- Use tenants for multi-workspace apps so users can have per-workspace notification preferences
- **Triggering workflows before identifying users** — Knock silently drops notifications if the recipient does not exist; always call `users.identify` during onboarding
- **Hardcoding channel routing in application code** — Let the Knock workflow definition handle channel routing and fallback logic; your app should only trigger the workflow with data
- **Not setting a cancellation key on cancellable workflows** — Without it, you cannot programmatically cancel in-flight delayed or scheduled workflow steps
- **Rendering raw notification data instead of using `blocks[].rendered`** — Knock pre-renders notification content server-side; use the rendered output for consistent display across channels
- **Ignoring the real-time connection lifecycle** — The feed client opens a WebSocket; dispose it when the component unmounts to avoid leaked connections and stale state

## Quick Example

```bash
# Server-side
npm install @knocklabs/node

# Client-side React
npm install @knocklabs/react
```
skilldb get messaging-services-skills/KnockFull skill: 262 lines
Paste into your CLAUDE.md or agent config

Knock — Messaging Integration

You are an expert in integrating Knock for in-app messaging, notification feeds, and cross-channel notification orchestration.

Core Philosophy

Overview

Knock is a notification infrastructure platform that provides APIs and pre-built UI components for delivering in-app messages, push notifications, email, SMS, and chat alerts through a unified workflow engine. It separates notification logic from application code, letting you define multi-step workflows with conditions, batching, delays, and channel routing in a managed service. The React SDK provides drop-in feed components; the server SDK triggers workflows and manages users.

Setup & Configuration

Install SDKs

# Server-side
npm install @knocklabs/node

# Client-side React
npm install @knocklabs/react

Server-Side Client

import Knock from "@knocklabs/node";

const knock = new Knock(process.env.KNOCK_API_KEY!);

// Identify a user so Knock knows who to notify
async function identifyUser(userId: string, name: string, email: string) {
  await knock.users.identify(userId, {
    name,
    email,
  });
}

// Set user channel data for push/chat integrations
async function setChannelData(userId: string, channelId: string, tokens: string[]) {
  await knock.users.setChannelData(userId, channelId, {
    tokens,
  });
}

Client-Side React Setup

import { KnockProvider, KnockFeedProvider, NotificationIconButton, NotificationFeedPopover } from "@knocklabs/react";
import "@knocklabs/react/dist/index.css";
import { useRef, useState } from "react";

function NotificationBell({ userId }: { userId: string }) {
  const [isVisible, setIsVisible] = useState(false);
  const buttonRef = useRef<HTMLButtonElement>(null);

  return (
    <KnockProvider
      apiKey={process.env.NEXT_PUBLIC_KNOCK_PUBLIC_API_KEY!}
      userId={userId}
    >
      <KnockFeedProvider feedId={process.env.NEXT_PUBLIC_KNOCK_FEED_CHANNEL_ID!}>
        <NotificationIconButton
          ref={buttonRef}
          onClick={() => setIsVisible(!isVisible)}
        />
        <NotificationFeedPopover
          buttonRef={buttonRef}
          isVisible={isVisible}
          onClose={() => setIsVisible(false)}
        />
      </KnockFeedProvider>
    </KnockProvider>
  );
}

Core Patterns

Triggering Workflows

// Trigger a workflow for one or more recipients
async function notifyNewMessage(
  senderId: string,
  recipientIds: string[],
  messageText: string,
  conversationId: string
) {
  await knock.workflows.trigger("new-message", {
    recipients: recipientIds,
    actor: senderId,
    data: {
      message: messageText,
      conversation_id: conversationId,
    },
    tenant: "my-workspace", // optional multi-tenancy
  });
}

// Trigger with inline identification (creates user if not exists)
async function notifyWithInlineUser(email: string, data: Record<string, unknown>) {
  await knock.workflows.trigger("welcome-flow", {
    recipients: [
      {
        id: email,
        email,
        name: data.name as string,
      },
    ],
    data,
  });
}

Managing Notification Preferences

// Server-side: set user preferences
async function setUserPreferences(userId: string) {
  await knock.users.setPreferences(userId, {
    channel_types: {
      email: true,
      sms: false,
      in_app_feed: true,
    },
    workflows: {
      "new-message": {
        channel_types: {
          email: false, // disable email for this workflow
        },
      },
      "system-alert": true,
    },
  });
}

// Get user preferences
async function getUserPreferences(userId: string) {
  const preferences = await knock.users.getPreferences(userId);
  return preferences;
}

Client-Side Feed Hooks

import { useKnockFeed, useNotifications } from "@knocklabs/react";

function CustomNotificationList() {
  const { feedClient } = useKnockFeed();
  const { items, loading } = useNotifications(feedClient);

  const markAsRead = (itemId: string) => {
    feedClient.markAsRead({ id: itemId });
  };

  const markAllRead = () => {
    feedClient.markAllAsRead();
  };

  const archiveItem = (itemId: string) => {
    feedClient.markAsArchived({ id: itemId });
  };

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <button onClick={markAllRead}>Mark all read</button>
      {items.map((item) => (
        <div key={item.id} className={item.read_at ? "read" : "unread"}>
          <p>{item.blocks[0]?.rendered}</p>
          <span>{new Date(item.inserted_at).toLocaleString()}</span>
          <button onClick={() => markAsRead(item.id)}>Mark read</button>
          <button onClick={() => archiveItem(item.id)}>Archive</button>
        </div>
      ))}
    </div>
  );
}

Batching Notifications

// Trigger events that Knock will batch based on workflow configuration
// In the Knock dashboard, configure the workflow step with a batch window

// Each call adds to the batch — Knock groups them by recipient + batch key
async function notifyCommentAdded(
  postAuthorId: string,
  commenterId: string,
  postTitle: string
) {
  await knock.workflows.trigger("new-comment", {
    recipients: [postAuthorId],
    actor: commenterId,
    data: {
      post_title: postTitle,
      commenter_name: commenterId,
    },
  });
  // If 5 people comment within the batch window, the user gets one
  // notification: "5 people commented on your post"
}

Managing Schedules

// Create scheduled notifications
async function scheduleDigest(userId: string) {
  await knock.workflows.trigger("daily-digest", {
    recipients: [userId],
    data: { type: "daily" },
    // Schedules are configured in the workflow definition on Knock dashboard
  });
}

// Cancel a workflow run
async function cancelWorkflow(cancellationKey: string, recipientIds: string[]) {
  await knock.workflows.cancel("reminder-flow", {
    cancellation_key: cancellationKey,
    recipients: recipientIds,
  });
}

Best Practices

  • Identify users in Knock during your sign-up or login flow so they are ready to receive notifications before any workflow triggers
  • Use the actor field when triggering workflows to attribute actions to a specific user, enabling "X did Y" style notification messages
  • Configure batching windows in the Knock dashboard for high-frequency events like comments or reactions to avoid notification fatigue
  • Use cancellation_key on workflows that may become irrelevant (e.g., reminders) so you can cancel them when the user takes the expected action
  • Leverage Knock's preference center components rather than building your own; they stay in sync with the workflow configuration automatically
  • Use tenants for multi-workspace apps so users can have per-workspace notification preferences

Common Pitfalls

  • Triggering workflows before identifying users — Knock silently drops notifications if the recipient does not exist; always call users.identify during onboarding
  • Hardcoding channel routing in application code — Let the Knock workflow definition handle channel routing and fallback logic; your app should only trigger the workflow with data
  • Not setting a cancellation key on cancellable workflows — Without it, you cannot programmatically cancel in-flight delayed or scheduled workflow steps
  • Rendering raw notification data instead of using blocks[].rendered — Knock pre-renders notification content server-side; use the rendered output for consistent display across channels
  • Ignoring the real-time connection lifecycle — The feed client opens a WebSocket; dispose it when the component unmounts to avoid leaked connections and stale state

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add messaging-services-skills

Get CLI access →