Claude Code for Knock: Developer Notification Platform — Claude Skills 360 Blog
Blog / Backend / Claude Code for Knock: Developer Notification Platform
Backend

Claude Code for Knock: Developer Notification Platform

Published: May 28, 2027
Read time: 6 min read
By: Claude Skills 360

Knock is a developer-first notification platform — new Knock(apiKey) creates the SDK. knock.notify(workflowKey, { actor, recipients, data }) triggers a notification workflow. knock.users.identify(userId, { name, email, phone_number }) upserts a user. Workflows compose multi-channel steps: email, SMS, push, in-app, chat — all configured in the Knock dashboard with Liquid templates. knock.notify("new-comment", { actor: authorId, recipients: [mentionedId], data: { postUrl, preview } }) is a typical call. @knocklabs/react-notification-feed provides KnockProvider, KnockFeedProvider, NotificationFeed, and NotificationIconButton with badge count. knock.users.setPreferences(userId, { workflows: { "new-comment": { channel_types: { email: false, in_app: true } } } }) controls per-workflow, per-channel preferences. knock.notifyBatch(workflowKey, notifications[]) sends to many recipients. Claude Code generates Knock workflow triggers, in-app feeds, preference centers, and multi-tenant notification routing.

CLAUDE.md for Knock

## Knock Stack
- Version: @knocklabs/node >= 0.6, @knocklabs/react-notification-feed >= 0.3
- Init: const knock = new Knock(process.env.KNOCK_API_KEY!)
- Trigger: await knock.notify("workflow-key", { actor: userId, recipients: [recipientId], data: { key: "value" } })
- Identify: await knock.users.identify(userId, { name, email, phone_number })
- Batch: await knock.notify("workflow", { actor, recipients: [id1, id2, ...ids] })
- Feed: <KnockProvider apiKey={KNOCK_PUBLIC_KEY} userId={userId}><KnockFeedProvider><NotificationFeed /></KnockFeedProvider></KnockProvider>
- Preferences: await knock.users.setPreferences(userId, { workflows: { "key": { channel_types: { email: false } } } })

Knock Server Utilities

// lib/notifications/knock.ts — Knock notification utilities
import { Knock } from "@knocklabs/node"

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

// Workflow keys — match keys in Knock dashboard
const WORKFLOWS = {
  welcome: "welcome",
  passwordReset: "password-reset",
  newComment: "new-comment",
  newFollower: "new-follower",
  orderConfirmed: "order-confirmed",
  weeklyDigest: "weekly-digest",
  teamInvite: "team-invite",
  mentionAlert: "mention-alert",
  billingAlert: "billing-alert",
} as const

type WorkflowKey = (typeof WORKFLOWS)[keyof typeof WORKFLOWS]

// ── User sync ──────────────────────────────────────────────────────────────

export async function syncKnockUser(user: {
  id: string
  email: string
  name: string
  phone?: string | null
  avatarUrl?: string | null
  plan?: string
}): Promise<void> {
  await knock.users.identify(user.id, {
    name: user.name,
    email: user.email,
    phone_number: user.phone ?? undefined,
    avatar: user.avatarUrl ?? undefined,
    // Custom properties accessible in workflow templates
    plan: user.plan ?? "free",
  })
}

// ── Notification triggers ──────────────────────────────────────────────────

export async function sendWelcomeNotification(userId: string, appName: string): Promise<void> {
  await knock.notify(WORKFLOWS.welcome, {
    actor: userId,
    recipients: [userId],
    data: {
      appName,
      loginUrl: `${process.env.APP_URL}/dashboard`,
    },
  })
}

export async function sendPasswordReset(userId: string, resetUrl: string): Promise<void> {
  await knock.notify(WORKFLOWS.passwordReset, {
    recipients: [userId],
    data: {
      resetUrl,
      expiresIn: "60 minutes",
    },
  })
}

export async function sendNewCommentNotification(params: {
  authorId: string
  recipientIds: string[]
  postTitle: string
  postUrl: string
  preview: string
}): Promise<void> {
  if (params.recipientIds.length === 0) return

  await knock.notify(WORKFLOWS.newComment, {
    actor: params.authorId,
    recipients: params.recipientIds,
    data: {
      postTitle: params.postTitle,
      postUrl: params.postUrl,
      preview: params.preview.slice(0, 200),
    },
  })
}

export async function sendMentionAlert(params: {
  mentionedByUserId: string
  mentionedUserId: string
  contextUrl: string
  preview: string
}): Promise<void> {
  await knock.notify(WORKFLOWS.mentionAlert, {
    actor: params.mentionedByUserId,
    recipients: [params.mentionedUserId],
    data: {
      contextUrl: params.contextUrl,
      preview: params.preview.slice(0, 200),
    },
  })
}

// ── Multi-tenant (SaaS) ────────────────────────────────────────────────────

export async function sendTeamInvite(params: {
  invitedByUserId: string
  recipientEmail: string
  teamId: string
  teamName: string
  inviteUrl: string
}): Promise<void> {
  // Recipient may not be a Knock user yet — use email object directly
  await knock.notify(WORKFLOWS.teamInvite, {
    actor: params.invitedByUserId,
    recipients: [{ email: params.recipientEmail }],
    tenant: params.teamId,
    data: {
      teamName: params.teamName,
      inviteUrl: params.inviteUrl,
    },
  })
}

// Notify all members of a team (multi-tenant workspace)
export async function notifyTeamMembers(params: {
  teamId: string
  actorId: string
  memberIds: string[]
  workflowKey: WorkflowKey
  data: Record<string, unknown>
}): Promise<void> {
  const recipients = params.memberIds.filter(id => id !== params.actorId)
  if (recipients.length === 0) return

  await knock.notify(params.workflowKey, {
    actor: params.actorId,
    recipients,
    tenant: params.teamId,
    data: params.data,
  })
}

// ── Bulk weekly digest ─────────────────────────────────────────────────────
export async function sendWeeklyDigest(userIds: string[]): Promise<void> {
  // Knock handles batch delivery internally — single call for all recipients
  await knock.notify(WORKFLOWS.weeklyDigest, {
    recipients: userIds,
    data: {
      digestUrl: `${process.env.APP_URL}/digest`,
    },
  })
}

In-App Notification Feed

// components/notifications/KnockInbox.tsx — Knock in-app feed
"use client"
import {
  KnockProvider,
  KnockFeedProvider,
  NotificationFeed,
  NotificationIconButton,
  useKnockFeed,
} from "@knocklabs/react-notification-feed"
import { useEffect, useRef, useState } from "react"

const KNOCK_PUBLIC_KEY = process.env.NEXT_PUBLIC_KNOCK_PUBLIC_KEY!

interface KnockInboxProps {
  userId: string
  userToken?: string  // JWT signed with KNOCK_SIGNING_KEY for production
}

export function KnockInbox({ userId, userToken }: KnockInboxProps) {
  const [isOpen, setIsOpen] = useState(false)
  const buttonRef = useRef<HTMLButtonElement>(null)

  return (
    <KnockProvider
      apiKey={KNOCK_PUBLIC_KEY}
      userId={userId}
      userToken={userToken}
    >
      <KnockFeedProvider
        feedId={process.env.NEXT_PUBLIC_KNOCK_FEED_CHANNEL_ID!}
        // Real-time updates via WebSocket
        defaultFeedOptions={{ archived: "exclude" }}
      >
        <div className="relative">
          <button
            ref={buttonRef}
            onClick={() => setIsOpen(prev => !prev)}
            className="relative p-2 rounded-full hover:bg-muted transition-colors"
          >
            <NotificationIconButton
              ref={buttonRef}
              onClick={() => setIsOpen(prev => !prev)}
            />
          </button>

          {isOpen && (
            <div className="absolute right-0 top-full mt-2 z-50 w-96 shadow-xl rounded-2xl border bg-popover overflow-hidden">
              <div className="flex items-center justify-between px-4 py-3 border-b">
                <h3 className="font-semibold text-sm">Notifications</h3>
                <button
                  onClick={() => setIsOpen(false)}
                  className="text-muted-foreground hover:text-foreground text-xs"
                >
                  Close
                </button>
              </div>

              <NotificationFeed
                onNotificationClick={(item) => {
                  const url = item.data?.url as string | undefined
                  if (url) {
                    window.location.href = url
                    setIsOpen(false)
                  }
                }}
                renderItem={(props) => <CustomNotificationItem {...props} />}
              />
            </div>
          )}
        </div>
      </KnockFeedProvider>
    </KnockProvider>
  )
}

// Custom feed item rendering
function CustomNotificationItem({ item, onItemClick }: any) {
  return (
    <div
      onClick={() => onItemClick(item)}
      className={`flex gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors ${
        !item.read_at ? "bg-blue-50/30 dark:bg-blue-950/20" : ""
      }`}
    >
      {item.actors?.[0]?.avatar && (
        <img
          src={item.actors[0].avatar}
          alt=""
          className="size-9 rounded-full flex-shrink-0 object-cover"
        />
      )}
      <div className="flex-1 min-w-0">
        <p
          className="text-sm leading-snug line-clamp-2"
          dangerouslySetInnerHTML={{ __html: item.blocks?.[0]?.rendered ?? item.data?.message ?? "" }}
        />
        <p className="text-xs text-muted-foreground mt-0.5">
          {new Date(item.inserted_at).toLocaleDateString()}
        </p>
      </div>
      {!item.read_at && (
        <div className="size-2 rounded-full bg-blue-500 flex-shrink-0 mt-1.5" />
      )}
    </div>
  )
}

Preferences API Route

// app/api/notifications/preferences/route.ts — manage notification preferences
import { NextRequest, NextResponse } from "next/server"
import { auth } from "@clerk/nextjs/server"
import { knock } from "@/lib/notifications/knock"

export async function GET() {
  const { userId } = await auth()
  if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })

  const preferences = await knock.users.getPreferences(userId)
  return NextResponse.json({ preferences })
}

export async function PUT(request: NextRequest) {
  const { userId } = await auth()
  if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })

  const { channel_types, workflows } = await request.json()

  await knock.users.setPreferences(userId, {
    channel_types,
    workflows,
  })

  return NextResponse.json({ ok: true })
}

For the Novu alternative when a fully open-source, self-hostable notification platform is required with the option to run on your own infrastructure and have no per-notification pricing — Novu stores workflows in your database while Knock is a fully managed service with a richer dashboard and better multi-tenancy for SaaS apps, see the Novu guide. For the Courier alternative when a visual notification studio with A/B testing notifications, analytics on delivery rates, and a large library of provider integrations (100+ channels) is preferred — Courier has a more visual workflow editor while Knock is code-first for developers, see the Courier guide. The Claude Skills 360 bundle includes Knock skill sets covering workflows, in-app feeds, and multi-tenant routing. Start with the free tier to try notification platform generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free