Skip to content

Conversation

@KMKoushik
Copy link
Member

@KMKoushik KMKoushik commented Jan 4, 2026


Summary by cubic

Adds first-class webhooks across the app with signed delivery, retries, logging, and a full dashboard UI. Also adds SDK helpers for signature verification and typed events.

  • New Features

    • Database: Webhook and WebhookCall models with statuses, metrics, and a 30‑day cleanup job.
    • Delivery: HMAC-SHA256 signatures with timestamp, exponential backoff, auto-disable on failures, pause/resume, test send, and per-call retry.
    • UI: Webhooks list, create/edit/delete, detail view with recent calls, payloads, filters, status badges, and retry.
    • API: tRPC router for CRUD, list calls with pagination/filtering, retry, test, and status changes.
    • Events: Email (SES), Contact, and Domain changes now emit typed webhook events.
    • SDK: New webhooks utilities (constructEvent, verifyWebhookSignature) with strong typing; README updated.
    • Shared lib: New @usesend/lib with webhook event types and SES error constants; updated imports.
    • Limits: Plan quotas for webhooks with server-side checks and limits API support.
  • Migration

    • pnpm install
    • pnpm prisma migrate deploy
    • Ensure Redis worker is running (new queues: webhook-dispatch, webhook-cleanup).
    • For local dev, pull the updated SES/SNS image: usesend/local-ses-sns:latest.

Written for commit 0116bb8. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Full Webhooks UI: create, edit, pause/resume, rotate secret, delete
    • Webhook details: call history, delivery status, payload viewer, retry & test actions
    • Automatic webhook delivery for contact, domain, and email events
    • SDK: webhook verification and typed event parsing
  • Updates

    • Added Webhooks to Settings navigation
    • Plan limits now include webhook quotas (enforced)

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Jan 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
unsend-marketing Ready Ready Preview, Comment Jan 4, 2026 10:43am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 4, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds end-to-end webhook support: new Prisma models and migration for Webhook and WebhookCall (enums, indexes, FKs); server-side WebhookService and queue/worker (enqueue, delivery, retry, signing, auto-disable); TRPC webhook router and API surface; emission points in contact, domain, and SES hook flows; plan limit integration and a webhook limit checker; cleanup job and queue constants; dashboard UI for listing, creating, editing, testing, and inspecting webhook calls; SDK webhook verification and event construction; new packages/lib and packages/sdk additions and related build/config updates.

Possibly related PRs

Suggested labels

hacktoberfest-accepted

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add webhooks' clearly and concisely summarizes the primary change—adding webhook functionality to the application. It is specific, follows conventional commit format, and accurately reflects the substantial feature addition evident in the changeset.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 issues found across 55 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx">

<violation number="1" location="apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx:220">
P2: Missing error state handling: The component doesn&#39;t check `webhookQuery.isError`. If the API call fails, users will see &quot;Webhook not found&quot; instead of an appropriate error message, which could be misleading.</violation>
</file>

<file name="apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx">

<violation number="1" location="apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx:118">
P2: Using `call.responseTimeMs &amp;&amp;` will hide the duration when it&#39;s 0ms, which is a valid response time. Use a null check instead.</violation>
</file>

<file name="packages/sdk/package.json">

<violation number="1" location="packages/sdk/package.json:28">
P0: Adding a private workspace package as a runtime dependency to a published SDK will break npm installations. `@usesend/lib` is marked `private: true` and won&#39;t be published to npm, causing dependency resolution failures for SDK users.

Consider either:
1. Moving `@usesend/lib` to `devDependencies` and bundling its code at build time (add `--noExternal @usesend/lib` to the tsup command)
2. Making `@usesend/lib` a public package that&#39;s also published to npm</violation>
</file>

<file name="apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx">

<violation number="1" location="apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx:90">
P2: Incorrect `colSpan` value. The table has 5 columns but `colSpan={6}` is used. This will cause layout inconsistency.</violation>
</file>

<file name="packages/sdk/README.md">

<violation number="1" location="packages/sdk/README.md:131">
P2: The Next.js example is missing error handling for `constructEvent()`. Unlike the Express example below which shows proper try/catch, this code will throw an unhandled error when signature verification fails, resulting in a 500 instead of a 400 response. Consider wrapping in try/catch for consistency:

```ts
export async function POST(request: Request) {
  try {
    const rawBody = await request.text();
    const event = constructEvent({
      secret: process.env.USESEND_WEBHOOK_SECRET!,
      headers: request.headers,
      rawBody,
    });
    // handle event...
    return new Response(&quot;ok&quot;);
  } catch (error) {
    return new Response((error as Error).message, { status: 400 });
  }
}
```</violation>
</file>

<file name="apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx">

<violation number="1" location="apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx:43">
P2: `navigator.clipboard.writeText` returns a Promise that can reject. Add error handling to avoid showing success toast when copy fails.</violation>
</file>

<file name="apps/web/src/components/code-display.tsx">

<violation number="1" location="apps/web/src/components/code-display.tsx:63">
P2: The `setTimeout` in `handleCopy` is not cleaned up on unmount, which can cause a React state update on an unmounted component. Consider using a ref to track the timeout and clear it in a cleanup function, or use a custom hook for this pattern.</violation>
</file>

<file name="apps/web/src/server/service/webhook-service.ts">

<violation number="1" location="apps/web/src/server/service/webhook-service.ts:783">
P2: Stream reader not canceled when truncating. When breaking early due to exceeding the response text limit, call `reader.cancel()` to properly release the underlying connection resources.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (20)
packages/lib/package.json (1)

10-12: Consider adding a build script if distributable artifacts are needed.

The package has no build script despite the tsconfig.json being configured to output to "dist". If this package is meant to be consumed as built artifacts rather than source files, you should add a build script.

Proposed build script addition
  "scripts": {
+   "build": "tsc",
    "lint": "eslint . --max-warnings 0",
    "lint:fix": "eslint . --fix"
  },
packages/sdk/README.md (1)

129-142: Suggest adding error handling to the Next.js example.

The Next.js App Route example currently lacks error handling, while the Express example (lines 169-183) includes a try-catch block. For webhook verification, explicit error handling is recommended because:

  1. Verification failures should return 400 (Bad Request) rather than 500 (Internal Server Error)
  2. Webhook senders use HTTP status codes to determine retry behavior
  3. Consistency between framework examples aids developer understanding

Consider adding a try-catch wrapper similar to the Express example to demonstrate proper error handling and return appropriate HTTP status codes for verification failures.

Suggested enhancement
 // In a Next.js App Route
 export async function POST(request: Request) {
-  const rawBody = await request.text(); // important: raw body, not parsed JSON
-  const event = constructEvent({
-    secret: process.env.USESEND_WEBHOOK_SECRET!,
-    headers: request.headers,
-    rawBody,
-  });
-
-  if (event.type === "email.delivered") {
-    // event.data is strongly typed here
+  try {
+    const rawBody = await request.text();
+    const event = constructEvent({
+      secret: process.env.USESEND_WEBHOOK_SECRET!,
+      headers: request.headers,
+      rawBody,
+    });
+
+    if (event.type === "email.delivered") {
+      // event.data is strongly typed here
+    }
+
+    return new Response("ok");
+  } catch (error) {
+    return new Response((error as Error).message, { status: 400 });
   }
-
-  return new Response("ok");
 }
apps/web/src/components/code-display.tsx (1)

44-49: Consider displaying raw code as fallback on highlighting failure.

When codeToHtml fails, html remains empty and isLoading becomes false, resulting in an empty container. Showing the raw code as a fallback would provide a better user experience.

🔎 Proposed fix
       } catch (error) {
         console.error("Failed to highlight code:", error);
         if (isMounted) {
+          setHtml(""); // Ensure html is empty to trigger fallback
           setIsLoading(false);
         }
       }

Then in the render section (around line 94):

+  if (!html) {
+    // Fallback: show raw code without highlighting
+    return (
+      <div className="relative rounded-lg overflow-hidden border bg-muted/50">
+        <Button
+          size="icon"
+          variant="ghost"
+          onClick={handleCopy}
+          className="absolute top-2 right-2 h-8 w-8 z-10"
+        >
+          {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
+        </Button>
+        <pre
+          className={`text-xs font-mono p-4 overflow-auto ${className}`}
+          style={{ maxHeight }}
+        >
+          <code className="p-2">{code}</code>
+        </pre>
+      </div>
+    );
+  }
+
   return (
apps/web/src/app/(dashboard)/webhooks/page.tsx (1)

4-5: Consider using the ~/ alias for consistency.

Per coding guidelines, imports in apps/web should use the ~/ alias instead of relative paths.

🔎 Suggested refactor
-import { AddWebhook } from "./add-webhook";
-import { WebhookList } from "./webhook-list";
+import { AddWebhook } from "~/app/(dashboard)/webhooks/add-webhook";
+import { WebhookList } from "~/app/(dashboard)/webhooks/webhook-list";

Based on coding guidelines: "Use alias ~/ for src imports in apps/web"

apps/web/src/server/jobs/webhook-cleanup-job.ts (1)

14-34: Consider batching large deletions for better database performance.

The deleteMany operation at line 18 could potentially delete millions of records in a single transaction if webhook calls have accumulated over time. This might cause:

  • Long-running transactions that lock the webhookCall table
  • Memory pressure from processing large result sets
  • Potential timeouts or connection issues
🔎 Suggested batched deletion approach
 const worker = new Worker(
   WEBHOOK_CLEANUP_QUEUE,
   async () => {
     const cutoff = subDays(new Date(), WEBHOOK_RETENTION_DAYS);
-    const result = await db.webhookCall.deleteMany({
-      where: {
-        createdAt: {
-          lt: cutoff,
-        },
-      },
-    });
+    
+    const BATCH_SIZE = 1000;
+    let totalDeleted = 0;
+    let hasMore = true;
+
+    while (hasMore) {
+      const result = await db.webhookCall.deleteMany({
+        where: {
+          id: {
+            in: (
+              await db.webhookCall.findMany({
+                where: { createdAt: { lt: cutoff } },
+                select: { id: true },
+                take: BATCH_SIZE,
+              })
+            ).map((r) => r.id),
+          },
+        },
+      });
+
+      totalDeleted += result.count;
+      hasMore = result.count === BATCH_SIZE;
+    }

     logger.info(
-      { deleted: result.count, cutoff: cutoff.toISOString() },
+      { deleted: totalDeleted, cutoff: cutoff.toISOString() },
       "[WebhookCleanupJob]: Deleted old webhook calls",
     );
   },
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (2)

25-40: Optimize the filtering and counting logic.

The current implementation:

  1. Filters calls to the last 7 days (line 26-28)
  2. Iterates three separate times to count different statuses (lines 30-40)

This can be optimized by combining the filtering and counting into a single pass, and by creating the sevenDaysAgo date object once instead of creating a new Date object for each call during filtering.

🔎 Proposed optimization
  const calls = callsQuery.data?.items ?? [];
- const last7DaysCalls = calls.filter(
-   (call) => new Date(call.createdAt) >= sevenDaysAgo,
- );
-
- const deliveredCount = last7DaysCalls.filter(
-   (c) => c.status === WebhookCallStatus.DELIVERED,
- ).length;
- const failedCount = last7DaysCalls.filter(
-   (c) => c.status === WebhookCallStatus.FAILED,
- ).length;
- const pendingCount = last7DaysCalls.filter(
-   (c) =>
-     c.status === WebhookCallStatus.PENDING ||
-     c.status === WebhookCallStatus.IN_PROGRESS,
- ).length;
+ 
+ const sevenDaysAgoTime = sevenDaysAgo.getTime();
+ let deliveredCount = 0;
+ let failedCount = 0;
+ let pendingCount = 0;
+ let last7DaysCalls = [];
+ 
+ for (const call of calls) {
+   if (new Date(call.createdAt).getTime() >= sevenDaysAgoTime) {
+     last7DaysCalls.push(call);
+     if (call.status === WebhookCallStatus.DELIVERED) {
+       deliveredCount++;
+     } else if (call.status === WebhookCallStatus.FAILED) {
+       failedCount++;
+     } else if (
+       call.status === WebhookCallStatus.PENDING ||
+       call.status === WebhookCallStatus.IN_PROGRESS
+     ) {
+       pendingCount++;
+     }
+   }
+ }

20-23: Add server-side date filtering to the listCalls TRPC endpoint.

The component fetches 50 webhook calls and filters them client-side to the last 7 days. The api.webhook.listCalls endpoint does not currently support date range parameters. Adding createdAfter and createdBefore parameters to the TRPC input schema and WebhookService.listWebhookCalls method would allow filtering on the server side, reducing unnecessary data transfer and improving performance.

apps/web/src/server/service/ses-hook-parser.ts (1)

397-428: Make the default case more explicit.

The default case returns "email.queued" for any unhandled status, including SCHEDULED. While SCHEDULED emails likely don't reach this code path via SES hooks, the implicit fallback could mask future issues when new statuses are added.

🔎 Proposed fix to handle SCHEDULED explicitly
 function emailStatusToEvent(status: EmailStatus): EmailWebhookEventType {
   switch (status) {
     case EmailStatus.QUEUED:
       return "email.queued";
     case EmailStatus.SENT:
       return "email.sent";
     case EmailStatus.DELIVERY_DELAYED:
       return "email.delivery_delayed";
     case EmailStatus.DELIVERED:
       return "email.delivered";
     case EmailStatus.BOUNCED:
       return "email.bounced";
     case EmailStatus.REJECTED:
       return "email.rejected";
     case EmailStatus.RENDERING_FAILURE:
       return "email.rendering_failure";
     case EmailStatus.COMPLAINED:
       return "email.complained";
     case EmailStatus.FAILED:
       return "email.failed";
     case EmailStatus.CANCELLED:
       return "email.cancelled";
     case EmailStatus.SUPPRESSED:
       return "email.suppressed";
     case EmailStatus.OPENED:
       return "email.opened";
     case EmailStatus.CLICKED:
       return "email.clicked";
+    case EmailStatus.SCHEDULED:
+      // SCHEDULED emails should not trigger webhooks via SES
+      logger.warn({ status }, "Unexpected SCHEDULED status in SES hook");
+      return "email.queued";
     default:
-      return "email.queued";
+      logger.error({ status }, "Unknown email status in emailStatusToEvent");
+      return "email.queued";
   }
 }
apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)

144-152: Consider rendering EditWebhookDialog unconditionally.

The current pattern conditionally mounts/unmounts the EditWebhookDialog component for each webhook. This causes the component to reinitialize on every open, which is less efficient. Since the dialog is already controlled via the open prop, you could render it once outside the map loop.

🔎 Proposed refactor to improve efficiency

Move the dialog outside the map loop and track the webhook being edited:

 export function WebhookList() {
   const webhooksQuery = api.webhook.list.useQuery();
   const setStatusMutation = api.webhook.setStatus.useMutation();
   const utils = api.useUtils();
   const router = useRouter();
-  const [editingId, setEditingId] = useState<string | null>(null);
+  const [editingWebhook, setEditingWebhook] = useState<Webhook | null>(null);

   const webhooks = webhooksQuery.data ?? [];

   // ... handlers ...

   return (
     <div className="mt-10">
       <div className="rounded-xl border shadow">
         <Table>
           {/* ... table header ... */}
           <TableBody>
             {/* ... loading/empty states ... */}
             {webhooks.map((webhook) => (
               <TableRow
                 key={webhook.id}
                 className="cursor-pointer hover:bg-muted/50"
                 onClick={() => router.push(`/webhooks/${webhook.id}`)}
               >
                 {/* ... table cells ... */}
                 <TableCell className="text-right">
                   <div
                     className="flex items-center justify-end"
                     onClick={(e) => e.stopPropagation()}
                   >
                     <WebhookActions
                       webhook={webhook}
-                      onEdit={() => setEditingId(webhook.id)}
+                      onEdit={() => setEditingWebhook(webhook)}
                       onToggleStatus={() =>
                         handleToggleStatus(webhook.id, webhook.status)
                       }
                       isToggling={setStatusMutation.isPending}
                     />
                   </div>
-                  {editingId === webhook.id ? (
-                    <EditWebhookDialog
-                      webhook={webhook}
-                      open={editingId === webhook.id}
-                      onOpenChange={(open) =>
-                        setEditingId(open ? webhook.id : null)
-                      }
-                    />
-                  ) : null}
                 </TableCell>
               </TableRow>
             ))}
           </TableBody>
         </Table>
       </div>
+      {editingWebhook && (
+        <EditWebhookDialog
+          webhook={editingWebhook}
+          open={!!editingWebhook}
+          onOpenChange={(open) => setEditingWebhook(open ? editingWebhook : null)}
+        />
+      )}
     </div>
   );
 }
apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx (2)

145-160: Unused formState variable in URL field render.

The formState is destructured but not used in this field's render function. Consider removing it to clean up the code.

Proposed fix
             <FormField
               control={form.control}
               name="url"
-              render={({ field, formState }) => (
+              render={({ field }) => (
                 <FormItem>
                   <FormLabel>Endpoint URL</FormLabel>

181-240: Consider extracting shared event selection logic.

The handleToggleAll, handleToggleGroup, handleToggleEvent, and isGroupFullySelected functions are nearly identical to those in add-webhook.tsx. Consider extracting these into a shared custom hook (e.g., useWebhookEventSelection) to reduce duplication.

apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (1)

151-166: Unused formState variable in URL field render.

Similar to the edit dialog, formState is destructured but not used in this field's render function.

Proposed fix
             <FormField
               control={form.control}
               name="url"
-              render={({ field, formState }) => (
+              render={({ field }) => (
                 <FormItem>
apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (2)

22-23: Remove unused import.

CircleEllipsis is imported but not used in the component.

Proposed fix
   Edit3,
   Key,
   MoreVertical,
   Pause,
   Play,
   TestTube,
-  CircleEllipsis,
 } from "lucide-react";

179-195: Use WebhookStatus enum for type safety.

The status comparison and mutation use string literals ("ACTIVE", "PAUSED") instead of the WebhookStatus enum. This could cause runtime issues if enum values change.

Proposed fix
+import { WebhookStatus } from "@prisma/client";

 const handleToggleStatus = (currentStatus: string) => {
-    const newStatus = currentStatus === "ACTIVE" ? "PAUSED" : "ACTIVE";
+    const newStatus = currentStatus === WebhookStatus.ACTIVE 
+      ? WebhookStatus.PAUSED 
+      : WebhookStatus.ACTIVE;
     setStatusMutation.mutate(
       { id: webhookId, status: newStatus },

Also update the function signature to use the enum type:

-  const handleToggleStatus = (currentStatus: string) => {
+  const handleToggleStatus = (currentStatus: WebhookStatus) => {
packages/sdk/src/webhooks.ts (2)

2-7: Consider using a proper package subpath export instead of importing from src/.

The import path @usesend/lib/src/webhook/webhook-events reaches into the package's internal src/ directory. This pattern can be fragile if the package structure changes and doesn't follow conventional package subpath exports.

Consider configuring a proper subpath export in @usesend/lib's package.json (e.g., @usesend/lib/webhook) to make this import more stable and explicit as part of the public API.


180-187: Document the negative tolerance behavior.

The condition toleranceMs >= 0 means passing a negative tolerance value disables the timestamp check entirely. While this may be intentional for testing scenarios, it's not documented in the JSDoc comments and could be misused.

Consider either documenting this behavior in the options type or removing the ability to disable tolerance checks via negative values.

apps/web/src/server/service/webhook-service.ts (3)

43-83: Consider lazy initialization for Queue and Worker.

The static initialization creates Redis connections immediately when the module is imported. This could cause issues in serverless/edge environments or during build time if the Redis connection isn't available.

Consider using a lazy initialization pattern (e.g., singleton getter) to defer connection until the first actual use.


235-240: Consider providing a user-friendly error message.

The error message uses reason ?? "Webhook limit reached" where reason is a LimitReason enum value. Depending on how LimitReason is defined, the message might display an internal enum name (e.g., "WEBHOOK") rather than a user-friendly message.

Consider mapping the LimitReason to a descriptive message string.


766-768: Consider extracting the skip threshold to a named constant.

The WEBHOOK_RESPONSE_TEXT_LIMIT * 2 is a magic number for determining when to skip reading the response entirely. Consider extracting this to a named constant for clarity (e.g., WEBHOOK_RESPONSE_SKIP_THRESHOLD).

packages/lib/src/webhook/webhook-events.ts (1)

151-165: Consider adding extended payload for email.complained.

Looking at the payload map, email.complained uses EmailBasePayload without complaint-specific details. If the complaint data (e.g., feedback type, complaint reason) is available from the email provider, consider adding an EmailComplainedPayload type similar to EmailBouncedPayload.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bba9e93 and 62d2baa.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (54)
  • apps/web/package.json
  • apps/web/prisma/migrations/20251122195838_add_webhook/migration.sql
  • apps/web/prisma/schema.prisma
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/server/api/routers/email.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/contact-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/tailwind.config.ts
  • docker/dev/compose.yml
  • packages/lib/.eslintrc.cjs
  • packages/lib/index.ts
  • packages/lib/package.json
  • packages/lib/src/constants/ses-errors.ts
  • packages/lib/src/index.ts
  • packages/lib/src/webhook/webhook-events.ts
  • packages/lib/tsconfig.json
  • packages/lib/tsconfig.lint.json
  • packages/sdk/README.md
  • packages/sdk/index.ts
  • packages/sdk/package.json
  • packages/sdk/src/usesend.ts
  • packages/sdk/src/webhooks.ts
  • packages/ui/src/dropdown-menu.tsx
  • packages/ui/styles/globals.css
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • packages/sdk/src/usesend.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • packages/sdk/index.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • packages/lib/src/index.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/server/api/routers/email.ts
  • apps/web/tailwind.config.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • packages/sdk/src/webhooks.ts
  • packages/lib/src/webhook/webhook-events.ts
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/server/service/contact-service.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • packages/sdk/src/usesend.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • packages/sdk/index.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • packages/lib/src/index.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/server/api/routers/email.ts
  • apps/web/tailwind.config.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • packages/sdk/src/webhooks.ts
  • packages/lib/src/webhook/webhook-events.ts
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/server/service/contact-service.ts
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

React components must use PascalCase naming convention (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • packages/ui/src/dropdown-menu.tsx
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/server/api/routers/email.ts
  • apps/web/tailwind.config.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • apps/web/src/server/service/contact-service.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/server/api/routers/email.ts
  • apps/web/tailwind.config.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • apps/web/src/server/service/contact-service.ts
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • packages/sdk/src/usesend.ts
  • packages/sdk/README.md
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • packages/sdk/index.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • packages/lib/src/index.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/server/api/routers/email.ts
  • apps/web/tailwind.config.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • packages/sdk/src/webhooks.ts
  • packages/lib/src/webhook/webhook-events.ts
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/server/service/contact-service.ts
🧠 Learnings (7)
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Run ESLint via usesend/eslint-config and ensure no warnings remain before submitting PRs

Applied to files:

  • packages/lib/package.json
  • packages/lib/.eslintrc.cjs
  • packages/lib/tsconfig.json
  • apps/web/tailwind.config.ts
  • packages/lib/tsconfig.lint.json
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages

Applied to files:

  • packages/lib/package.json
  • packages/lib/.eslintrc.cjs
  • packages/lib/tsconfig.json
  • apps/web/tailwind.config.ts
  • packages/lib/tsconfig.lint.json
📚 Learning: 2025-11-28T21:13:56.758Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: .cursor/rules/general.mdc:0-0
Timestamp: 2025-11-28T21:13:56.758Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : Include all required imports and ensure proper naming of key components in React/NextJS code

Applied to files:

  • apps/web/src/components/code-display.tsx
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/tailwind.config.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web

Applied to files:

  • apps/web/src/server/api/routers/webhook.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx,js,jsx} : Use alias `~/` for src imports in apps/web (e.g., `import { x } from "~/utils/x"`)

Applied to files:

  • apps/web/tailwind.config.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,md} : Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Applied to files:

  • apps/web/tailwind.config.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Use Prisma for database access in apps/web

Applied to files:

  • apps/web/prisma/schema.prisma
🧬 Code graph analysis (21)
apps/web/src/app/(dashboard)/webhooks/page.tsx (2)
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (1)
  • AddWebhook (66-333)
apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)
  • WebhookList (29-162)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (4)
packages/ui/src/toaster.tsx (1)
  • toast (31-31)
packages/ui/src/badge.tsx (1)
  • Badge (36-36)
apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx (1)
  • WebhookStatusBadge (1-23)
packages/ui/src/button.tsx (1)
  • Button (80-80)
apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx (1)
apps/web/src/components/DeleteResource.tsx (1)
  • DeleteResource (63-199)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (5)
packages/ui/src/select.tsx (5)
  • Select (150-150)
  • SelectTrigger (153-153)
  • SelectValue (152-152)
  • SelectContent (154-154)
  • SelectItem (156-156)
packages/ui/src/table.tsx (5)
  • Table (109-109)
  • TableRow (114-114)
  • TableHead (113-113)
  • TableBody (111-111)
  • TableCell (115-115)
packages/ui/src/spinner.tsx (1)
  • Spinner (4-51)
apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (1)
  • WebhookCallStatusBadge (3-41)
packages/ui/src/button.tsx (1)
  • Button (80-80)
apps/web/src/server/api/routers/contacts.ts (1)
apps/web/src/server/api/trpc.ts (1)
  • contactBookProcedure (204-222)
apps/web/src/server/public-api/api/contacts/delete-contact.ts (1)
apps/web/src/server/service/contact-service.ts (1)
  • deleteContact (97-107)
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (4)
packages/lib/src/webhook/webhook-events.ts (5)
  • WebhookEvents (36-40)
  • WebhookEventType (42-42)
  • ContactEvents (1-5)
  • DomainEvents (9-14)
  • EmailEvents (18-32)
packages/sdk/index.ts (1)
  • WebhookEventType (16-16)
packages/sdk/src/webhooks.ts (1)
  • WebhookEventType (278-278)
apps/web/src/store/upgradeModalStore.ts (1)
  • useUpgradeModalStore (13-20)
packages/sdk/src/usesend.ts (2)
packages/sdk/index.ts (1)
  • Webhooks (5-5)
packages/sdk/src/webhooks.ts (1)
  • Webhooks (45-199)
apps/web/src/server/public-api/api/contacts/update-contact.ts (1)
apps/web/src/server/service/contact-service.ts (1)
  • updateContact (80-95)
apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (3)
apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx (1)
  • WebhookStatusBadge (1-23)
apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx (1)
  • EditWebhookDialog (66-323)
apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx (1)
  • DeleteWebhook (11-61)
apps/web/src/server/jobs/webhook-cleanup-job.ts (4)
apps/web/src/server/queue/queue-constants.ts (2)
  • WEBHOOK_CLEANUP_QUEUE (7-7)
  • DEFAULT_QUEUE_OPTIONS (9-14)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/api/routers/webhook.ts (2)
apps/web/src/server/api/trpc.ts (2)
  • createTRPCRouter (82-82)
  • teamProcedure (127-152)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (85-385)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/webhook.ts (1)
  • webhookRouter (9-135)
apps/web/src/server/service/contact-queue-service.ts (1)
apps/web/src/server/service/contact-service.ts (1)
  • addOrUpdateContact (19-78)
apps/web/src/server/service/domain-service.ts (2)
packages/lib/src/webhook/webhook-events.ts (2)
  • DomainPayload (86-100)
  • DomainWebhookEventType (16-16)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (85-385)
apps/web/src/server/api/routers/limits.ts (1)
apps/web/src/server/service/limit-service.ts (1)
  • LimitService (19-280)
apps/web/src/server/service/ses-hook-parser.ts (3)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (85-385)
packages/lib/src/webhook/webhook-events.ts (3)
  • EmailEventPayloadMap (151-165)
  • EmailStatus (44-58)
  • EmailBasePayload (60-72)
apps/web/src/types/aws-types.ts (4)
  • SesEvent (143-156)
  • SesEventDataKey (132-141)
  • SesBounce (30-50)
  • SesClick (91-97)
packages/sdk/src/webhooks.ts (2)
packages/sdk/index.ts (7)
  • WebhookVerificationError (6-6)
  • WEBHOOK_SIGNATURE_HEADER (9-9)
  • WEBHOOK_TIMESTAMP_HEADER (10-10)
  • WEBHOOK_EVENT_HEADER (7-7)
  • WEBHOOK_CALL_HEADER (8-8)
  • Webhooks (5-5)
  • WebhookEventData (14-14)
packages/lib/src/webhook/webhook-events.ts (1)
  • WebhookEventData (194-196)
apps/web/src/server/service/webhook-service.ts (6)
apps/web/src/server/queue/bullmq-context.ts (2)
  • TeamJob (5-5)
  • createWorkerHandler (10-24)
packages/lib/src/webhook/webhook-events.ts (2)
  • WebhookEventType (42-42)
  • WebhookPayloadData (184-185)
apps/web/src/server/queue/queue-constants.ts (2)
  • WEBHOOK_DISPATCH_QUEUE (6-6)
  • DEFAULT_QUEUE_OPTIONS (9-14)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/server/public-api/api-error.ts (1)
  • UnsendApiError (62-75)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx (1)
packages/ui/src/code-block.tsx (1)
  • CodeBlock (13-36)
apps/web/src/server/service/contact-service.ts (4)
apps/web/src/server/db.ts (1)
  • db (20-20)
packages/lib/src/webhook/webhook-events.ts (2)
  • ContactWebhookEventType (7-7)
  • ContactPayload (74-84)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (85-385)
🪛 ast-grep (0.40.3)
apps/web/src/components/code-display.tsx

[warning] 106-106: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
apps/web/src/components/code-display.tsx

[error] 107-107: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Cloudflare Pages

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jan 4, 2026

Deploying usesend with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0116bb8
Status: ✅  Deploy successful!
Preview URL: https://8e47a651.usesend.pages.dev
Branch Preview URL: https://km-2025-11-17-webhook.usesend.pages.dev

View logs

stuff

webhook call changes

more stuff

stuff

stuff

more webhook stuff

add pnpm

more ui stuff

stuff

sdk stuff

stuff

webhook ui

stuff

stuff

stuff

fix: address code review issues in webhooks and SDK

- Fix colSpan values in webhook tables to match actual column counts
- Remove unused Card component imports from webhook-info.tsx
- Use WebhookStatus enum instead of string literals in webhook-status-badge
- Fix JSDoc example in usesend.ts to use correct constructEvent signature
- Cancel stream reader when truncating response in webhook-service
- Add error handling to Next.js webhook example in SDK README
- Move @usesend/lib to devDependencies and bundle at build time
- Use null check for responseTimeMs to handle 0ms response times

other changes

update lock
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (1)

113-113: LGTM! The colSpan fix has been applied.

The previous issue with colSpan={5} has been corrected to colSpan={3}, which now matches the actual number of columns in the table.

Also applies to: 122-122

apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)

31-31: Remove unused mutation import.

The testWebhook mutation is imported but never used in the component. This appears to be leftover from when the handleTest function was removed.

🔎 Proposed fix
 export function WebhookList() {
   const webhooksQuery = api.webhook.list.useQuery();
-  const testWebhook = api.webhook.test.useMutation();
   const setStatusMutation = api.webhook.setStatus.useMutation();
🧹 Nitpick comments (3)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (2)

99-157: Consider using a single table for better column alignment and accessibility.

The current implementation splits the table into two separate <Table> components—one for the header and one for the body—to achieve a sticky header with scrollable content. While this works functionally, it can lead to:

  1. Column width misalignment between header and body (not guaranteed to stay in sync)
  2. Semantic HTML issues (header and body should be in the same table element)
  3. Accessibility concerns for screen readers expecting a unified table structure
🔎 Recommended approach using a single table

Consider using a single <Table> with CSS to make the header sticky:

-      <div className="flex-1 overflow-hidden rounded-xl border shadow flex flex-col">
-        <Table>
-          <TableHeader className="sticky top-0 z-10">
-            <TableRow className="bg-muted dark:bg-muted/70">
-              <TableHead className="h-9 rounded-tl-xl">Status</TableHead>
-              <TableHead className="h-9">Event Type</TableHead>
-              <TableHead className="h-9 rounded-tr-xl">Time</TableHead>
-            </TableRow>
-          </TableHeader>
-        </Table>
-        <div className="flex-1 overflow-auto no-scrollbar">
-          <Table>
-            <TableBody>
+      <div className="flex-1 overflow-auto no-scrollbar rounded-xl border shadow">
+        <Table>
+          <TableHeader className="sticky top-0 z-10 bg-muted dark:bg-muted/70">
+            <TableRow>
+              <TableHead className="h-9">Status</TableHead>
+              <TableHead className="h-9">Event Type</TableHead>
+              <TableHead className="h-9">Time</TableHead>
+            </TableRow>
+          </TableHeader>
+          <TableBody>
               {/* ... table body content ... */}
-            </TableBody>
-          </Table>
-        </div>
+          </TableBody>
+        </Table>
       </div>

This approach maintains column alignment and semantic correctness. If the rounded corners on the header are important, they can be achieved with additional CSS targeting the first/last TableHead elements.


111-119: Consider adding ARIA labels to the loading spinner for screen reader users.

While the spinner is visually clear, screen reader users would benefit from an explicit loading announcement.

🔎 Suggested enhancement
                   <TableCell colSpan={3} className="py-4 text-center">
+                    <div role="status" aria-live="polite" aria-label="Loading webhook calls">
                       <Spinner
                         className="mx-auto h-6 w-6"
                         innerSvgClass="stroke-primary"
                       />
+                    </div>
                   </TableCell>

Alternatively, add aria-busy="true" to the table wrapper during loading and include visually hidden text.

references/webhook-architecture.md (1)

7-129: Consider adding language identifiers to fenced code blocks.

The architecture diagram and state machine use fenced code blocks without language identifiers. While markdownlint flags this, these ASCII art diagrams don't have appropriate language tags. You could use text or ascii to satisfy the linter while maintaining readability.

🔎 Example fix for the architecture diagram
-```
+```text
 ┌─────────────────────────────────────────────────────────────────────────────────────┐
 │                                    EVENT SOURCES                                     │
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 62d2baa and 044e408.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • apps/web/prisma/migrations/20251122195838_add_webhook/migration.sql
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/service/webhook-service.ts
  • packages/sdk/README.md
  • packages/sdk/package.json
  • packages/sdk/src/usesend.ts
  • references/webhook-architecture.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • packages/sdk/src/usesend.ts
  • packages/sdk/package.json
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • packages/sdk/README.md
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/service/webhook-service.ts
  • references/webhook-architecture.md
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/service/webhook-service.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/service/webhook-service.ts
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

React components must use PascalCase naming convention (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/service/webhook-service.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/service/webhook-service.ts
🧬 Code graph analysis (1)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (2)
packages/ui/src/spinner.tsx (1)
  • Spinner (4-51)
apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (1)
  • WebhookCallStatusBadge (3-41)
🪛 markdownlint-cli2 (0.18.1)
references/webhook-architecture.md

7-7: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


133-133: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


173-173: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


320-320: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


364-364: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (9)
packages/sdk/README.md (1)

118-193: LGTM! Webhook documentation is comprehensive and accurate.

The webhook verification examples now include proper error handling in both Next.js and Express, headers are well-documented, and the typed event usage is clearly demonstrated.

apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)

39-212: LGTM! Component logic is well-structured.

The status toggle handler, table rendering with correct colSpan={5}, row navigation, and actions menu are all implemented correctly. Good use of event propagation control to prevent row clicks when interacting with the actions popover.

apps/web/prisma/migrations/20251122195838_add_webhook/migration.sql (1)

1-66: LGTM! Database migration is well-structured.

The migration correctly:

  • Defines comprehensive status enums for webhooks and calls
  • Creates properly indexed tables with appropriate foreign key constraints
  • Uses CASCADE for cleanup and SET NULL for optional references
  • Includes composite indexes optimized for common query patterns (teamId + webhookId + status)
apps/web/src/server/service/webhook-service.ts (6)

42-82: LGTM! Queue service initialization is clean.

The static initialization pattern for BullMQ queue and worker is appropriate, with proper error handling and observability via logging. Configuration constants are well-defined at the module level.


84-133: LGTM! Event emission logic is robust.

The emit method correctly:

  • Filters for ACTIVE webhooks with matching event subscriptions (including "all events" via empty array)
  • Persists WebhookCall records before enqueueing for reliability
  • Handles payload serialization safely

135-390: LGTM! Service methods follow solid patterns.

The retry, test, and CRUD methods demonstrate:

  • Consistent error handling with appropriate UnsendApiError codes
  • Proper team-based scoping for multi-tenancy security
  • Limit enforcement on webhook creation
  • Safe status transitions (resetting failure counters on activation)

409-574: LGTM! Webhook delivery worker is well-implemented.

The processWebhookCall function handles the complete delivery lifecycle correctly:

  • Discards calls for inactive webhooks
  • Acquires per-webhook locks to ensure ordered delivery
  • Handles both success and failure paths with proper status updates
  • Implements exponential backoff with attempt limits
  • Auto-disables webhooks after consecutive failure threshold
  • Captures response metadata for observability

393-407: LGTM! Helper functions are correctly implemented.

The utility functions demonstrate best practices:

  • stringifyPayload with safe error handling and fallback
  • acquireLock using Redis SET NX PX pattern
  • releaseLock with Lua script for atomic check-and-delete
  • computeBackoff with 30% jitter to prevent thundering herd
  • buildPayload with safe JSON parsing

Also applies to: 576-648


650-821: LGTM! HTTP delivery and signing implementation is robust.

The webhook HTTP POST logic is well-implemented:

  • AbortController properly handles request timeouts
  • HMAC-SHA256 signature follows standard format (v1=<hex>)
  • Response capture handles both direct text and streaming bodies
  • Stream reader is properly canceled when truncating (line 800) ✓
  • WebhookHttpError captures response metadata for diagnostics

Comment on lines +130 to +137
<TableRow
key={call.id}
className={`cursor-pointer transition-colors ${
selectedCallId === call.id
? "bg-accent/50 text-accent-foreground"
: "hover:bg-muted/50"
}`}
onClick={() => onSelectCall(call.id)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add keyboard accessibility to clickable table rows.

The table rows are clickable but lack keyboard accessibility. Users navigating with a keyboard cannot interact with these rows because they're missing tabIndex, onKeyDown, and appropriate ARIA attributes.

🔎 Proposed fix to add keyboard support
                 calls.map((call) => (
                   <TableRow
                     key={call.id}
+                    role="button"
+                    tabIndex={0}
                     className={`cursor-pointer transition-colors ${
                       selectedCallId === call.id
                         ? "bg-accent/50 text-accent-foreground"
                         : "hover:bg-muted/50"
                     }`}
                     onClick={() => onSelectCall(call.id)}
+                    onKeyDown={(e) => {
+                      if (e.key === "Enter" || e.key === " ") {
+                        e.preventDefault();
+                        onSelectCall(call.id);
+                      }
+                    }}
                   >

This enables keyboard users to:

  • Tab to each row
  • Press Enter or Space to select a webhook call
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<TableRow
key={call.id}
className={`cursor-pointer transition-colors ${
selectedCallId === call.id
? "bg-accent/50 text-accent-foreground"
: "hover:bg-muted/50"
}`}
onClick={() => onSelectCall(call.id)}
<TableRow
key={call.id}
role="button"
tabIndex={0}
className={`cursor-pointer transition-colors ${
selectedCallId === call.id
? "bg-accent/50 text-accent-foreground"
: "hover:bg-muted/50"
}`}
onClick={() => onSelectCall(call.id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onSelectCall(call.id);
}
}}
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
around lines 130-137, the clickable TableRow lacks keyboard accessibility; add
tabIndex={0} so rows are focusable, add an onKeyDown handler that invokes
onSelectCall(call.id) when Enter or Space is pressed, and include appropriate
ARIA attributes (e.g., role="button" and aria-selected={selectedCallId ===
call.id}) so assistive tech knows the row is interactive and its selection
state; keep the existing onClick behavior intact.

Comment on lines +449 to +450
// TODO: perform signed HTTP POST with backoff tracking and update status/metrics.
// This stub ensures call rows and queue wiring exist before adding delivery logic.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove outdated TODO comment.

This TODO suggests the code is a stub, but the full webhook delivery implementation is present (signed HTTP POST, backoff tracking, status updates, auto-disable logic). The comment is misleading and should be removed.

🔎 Proposed fix
-  // TODO: perform signed HTTP POST with backoff tracking and update status/metrics.
-  // This stub ensures call rows and queue wiring exist before adding delivery logic.
   const lockKey = `webhook:lock:${call.webhookId}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// TODO: perform signed HTTP POST with backoff tracking and update status/metrics.
// This stub ensures call rows and queue wiring exist before adding delivery logic.
const lockKey = `webhook:lock:${call.webhookId}`;
🤖 Prompt for AI Agents
In apps/web/src/server/service/webhook-service.ts around lines 449 to 450,
remove the outdated TODO comment that claims webhook delivery is a stub; delete
that comment line(s) so the codebase no longer contains misleading TODOs and
leave any actual implementation or explanatory comments that accurately reflect
the present signed HTTP POST, backoff tracking, status updates, and auto-disable
behavior.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (4)
packages/lib/tsconfig.lint.json (1)

6-6: Remove the non-existent "turbo" directory from the include array.

As flagged in a previous review, the "turbo" directory does not exist in packages/lib and should be removed from the include array.

🔎 Proposed fix
-  "include": ["src", "turbo", "**/*.ts", "**/*.tsx"],
+  "include": ["src", "**/*.ts", "**/*.tsx"],
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (1)

41-44: Add error handling for clipboard operation.

navigator.clipboard.writeText returns a Promise that can reject (e.g., due to permissions). The success toast will show even if the copy fails.

🔎 Proposed fix
-  const handleCopySecret = () => {
-    navigator.clipboard.writeText(webhook.secret);
-    toast.success("Secret copied to clipboard");
+  const handleCopySecret = async () => {
+    try {
+      await navigator.clipboard.writeText(webhook.secret);
+      toast.success("Secret copied to clipboard");
+    } catch {
+      toast.error("Failed to copy secret");
+    }
   };
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (1)

130-152: Add keyboard accessibility to clickable table rows.

The table rows are interactive via click but cannot be accessed via keyboard navigation. This is an accessibility concern for users who navigate with keyboards.

🔎 Proposed fix
                   <TableRow
                     key={call.id}
+                    role="button"
+                    tabIndex={0}
                     className={`cursor-pointer transition-colors ${
                       selectedCallId === call.id
                         ? "bg-accent/50 text-accent-foreground"
                         : "hover:bg-muted/50"
                     }`}
                     onClick={() => onSelectCall(call.id)}
+                    onKeyDown={(e) => {
+                      if (e.key === "Enter" || e.key === " ") {
+                        e.preventDefault();
+                        onSelectCall(call.id);
+                      }
+                    }}
                   >
apps/web/src/server/service/webhook-service.ts (1)

449-450: Remove outdated TODO comment.

This TODO claims the code is a stub, but the full webhook delivery implementation is present (signed HTTP POST, backoff tracking, status updates, auto-disable logic). The comment is misleading.

🔎 Proposed fix
-  // TODO: perform signed HTTP POST with backoff tracking and update status/metrics.
-  // This stub ensures call rows and queue wiring exist before adding delivery logic.
   const lockKey = `webhook:lock:${call.webhookId}`;
🧹 Nitpick comments (12)
references/webhook-architecture.md (1)

7-7: Consider adding language specifiers to fenced code blocks.

Several fenced code blocks lack language specifiers, which can affect rendering and tooling support. Consider adding appropriate tags:

  • Lines 7, 133: Use ```text for ASCII diagrams
  • Line 173: Use ```text for schema definitions
  • Line 320: Use ```http or ```text for HTTP headers
  • Line 364: Use ```text for the status flow

This improves syntax highlighting and markdown linting compliance.

Example fix for the first diagram
-```
+```text
 ┌─────────────────────────────────────────────────────────────────────────────────────┐
 │                                    EVENT SOURCES                                     │

Also applies to: 133-133, 173-173, 320-320, 364-364

apps/web/src/server/service/ses-hook-parser.ts (2)

372-395: Consider deriving valid subtypes from the type definition.

The function correctly normalizes bounce subtypes, but the validSubTypes array is hardcoded and could drift from the EmailBounceSubType definition over time.

💡 Alternative approach using type-level validation

If the webhook events library exports the valid subtypes as a constant array, you could import and reuse it here to maintain a single source of truth. However, if the type is only available at compile-time, the current approach is acceptable with proper documentation.


430-489: Add explicit return type annotation for type safety.

The function currently has an implicit any return type. Consider adding an explicit return type annotation for better type safety and documentation.

🔎 Suggested improvement
 function buildEmailMetadata(
   status: EmailStatus,
   mailData: SesEvent | SesEvent[SesEventDataKey],
-) {
+): Record<string, unknown> | undefined {

This makes the return type explicit and prevents accidental any type propagation.

apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (2)

154-154: Remove unused formState parameter.

The formState parameter is destructured but never used in this render function.

🔎 Proposed fix
-              render={({ field, formState }) => (
+              render={({ field }) => (

128-133: Simplify onOpenChange handler.

The check nextOpen !== open is unnecessary since onOpenChange is only called when the dialog state actually changes.

🔎 Proposed fix
     <Dialog
       open={open}
-      onOpenChange={(nextOpen) =>
-        nextOpen !== open ? onOpenChange(nextOpen) : null
-      }
+      onOpenChange={onOpenChange}
     >
apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx (2)

16-16: Remove unused import FormDescription.

FormDescription is imported but never used in this file.

🔎 Proposed fix
 import {
   Form,
   FormControl,
-  FormDescription,
   FormField,
   FormItem,
   FormLabel,
   FormMessage,
 } from "@usesend/ui/src/form";

148-148: Remove unused formState parameter.

The formState parameter is destructured but never used in this render function.

🔎 Proposed fix
-              render={({ field, formState }) => (
+              render={({ field }) => (
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (2)

78-78: Use WebhookCallStatus enum instead of string literal.

For consistency and type safety, import and use the WebhookCallStatus enum rather than the string literal "FAILED".

🔎 Proposed fix
+import { WebhookCallStatus } from "@prisma/client";
+
 // ...
 
-        {call.status === "FAILED" && (
+        {call.status === WebhookCallStatus.FAILED && (

163-170: Remove unnecessary fragment wrapper.

The fragment <>...</> wraps only a single <div> child, making it redundant.

🔎 Proposed fix
         {call.responseText && (
-          <>
-            <div className="flex flex-col gap-3">
-              <h4 className="font-medium text-sm">Response Body</h4>
-              <CodeDisplay code={call.responseText} />
-            </div>
-          </>
+          <div className="flex flex-col gap-3">
+            <h4 className="font-medium text-sm">Response Body</h4>
+            <CodeDisplay code={call.responseText} />
+          </div>
         )}
apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (2)

22-22: Remove unused import CircleEllipsis.

The CircleEllipsis icon is imported but never used in the component.

🔎 Proposed fix
 import {
   Edit3,
   Key,
   MoreVertical,
   Pause,
   Play,
   TestTube,
-  CircleEllipsis,
 } from "lucide-react";

57-58: Consider using WebhookStatus enum for status comparisons.

Lines 57-58 and line 180 use string literals for status comparisons. Using the Prisma-generated enum would provide better type safety.

🔎 Proposed fix
+import { WebhookStatus } from "@prisma/client";

 // In WebhookDetailActions:
-  const isPaused = webhook.status === "PAUSED";
-  const isAutoDisabled = webhook.status === "AUTO_DISABLED";
+  const isPaused = webhook.status === WebhookStatus.PAUSED;
+  const isAutoDisabled = webhook.status === WebhookStatus.AUTO_DISABLED;

 // In handleToggleStatus:
-    const newStatus = currentStatus === "ACTIVE" ? "PAUSED" : "ACTIVE";
+    const newStatus = currentStatus === WebhookStatus.ACTIVE ? WebhookStatus.PAUSED : WebhookStatus.ACTIVE;
packages/sdk/src/webhooks.ts (1)

180-187: Consider documenting the negative tolerance escape hatch.

Setting tolerance to a negative value disables timestamp validation. While this can be useful for testing, it might be worth documenting this behavior in the JSDoc or adding a named constant like SKIP_TIMESTAMP_VALIDATION = -1.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 044e408 and 0116bb8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (55)
  • apps/web/package.json
  • apps/web/prisma/migrations/20251122195838_add_webhook/migration.sql
  • apps/web/prisma/schema.prisma
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/src/server/api/routers/email.ts
  • apps/web/src/server/api/routers/limits.ts
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/contact-service.ts
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/tailwind.config.ts
  • docker/dev/compose.yml
  • packages/lib/.eslintrc.cjs
  • packages/lib/index.ts
  • packages/lib/package.json
  • packages/lib/src/constants/ses-errors.ts
  • packages/lib/src/index.ts
  • packages/lib/src/webhook/webhook-events.ts
  • packages/lib/tsconfig.json
  • packages/lib/tsconfig.lint.json
  • packages/sdk/README.md
  • packages/sdk/index.ts
  • packages/sdk/package.json
  • packages/sdk/src/usesend.ts
  • packages/sdk/src/webhooks.ts
  • packages/ui/src/dropdown-menu.tsx
  • packages/ui/styles/globals.css
  • references/webhook-architecture.md
🚧 Files skipped from review as they are similar to previous changes (27)
  • packages/sdk/package.json
  • apps/web/src/app/(dashboard)/webhooks/page.tsx
  • apps/web/src/server/api/routers/email.ts
  • packages/sdk/README.md
  • apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
  • apps/web/src/server/public-api/api/contacts/delete-contact.ts
  • docker/dev/compose.yml
  • apps/web/tailwind.config.ts
  • packages/lib/tsconfig.json
  • packages/lib/src/index.ts
  • packages/sdk/index.ts
  • apps/web/src/server/public-api/api/contacts/update-contact.ts
  • apps/web/src/app/(dashboard)/emails/email-details.tsx
  • apps/web/src/server/public-api/api/contacts/upsert-contact.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
  • packages/lib/package.json
  • apps/web/src/app/(dashboard)/dev-settings/layout.tsx
  • apps/web/src/server/service/contact-service.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
  • apps/web/src/server/jobs/webhook-cleanup-job.ts
  • apps/web/src/server/public-api/api/contacts/add-contact.ts
  • apps/web/src/server/api/routers/contacts.ts
  • apps/web/prisma/migrations/20251122195838_add_webhook/migration.sql
  • packages/ui/styles/globals.css
  • apps/web/src/server/service/limit-service.ts
  • apps/web/src/components/AppSideBar.tsx
  • packages/lib/src/webhook/webhook-events.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • packages/sdk/src/usesend.ts
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/service/contact-queue-service.ts
  • packages/sdk/src/webhooks.ts
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/server/api/routers/limits.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • packages/sdk/src/usesend.ts
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/service/contact-queue-service.ts
  • packages/sdk/src/webhooks.ts
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/server/api/routers/limits.ts
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

React components must use PascalCase naming convention (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/components/code-display.tsx
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/server/api/routers/limits.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/service/contact-queue-service.ts
  • apps/web/src/server/service/webhook-service.ts
  • apps/web/src/server/api/routers/limits.ts
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
  • apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/queue/queue-constants.ts
  • apps/web/src/lib/constants/plans.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
  • packages/ui/src/dropdown-menu.tsx
  • apps/web/src/server/service/domain-service.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx
  • packages/sdk/src/usesend.ts
  • apps/web/src/server/api/routers/webhook.ts
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/server/service/ses-hook-parser.ts
  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx
  • apps/web/src/components/code-display.tsx
  • apps/web/src/server/service/contact-queue-service.ts
  • packages/sdk/src/webhooks.ts
  • apps/web/src/server/service/webhook-service.ts
  • references/webhook-architecture.md
  • apps/web/src/server/api/routers/limits.ts
🧠 Learnings (5)
📚 Learning: 2025-11-28T21:13:56.758Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: .cursor/rules/general.mdc:0-0
Timestamp: 2025-11-28T21:13:56.758Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : Include all required imports and ensure proper naming of key components in React/NextJS code

Applied to files:

  • apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
  • apps/web/src/components/code-display.tsx
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web

Applied to files:

  • apps/web/src/server/api/routers/webhook.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages

Applied to files:

  • packages/lib/tsconfig.lint.json
  • packages/lib/.eslintrc.cjs
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Run ESLint via usesend/eslint-config and ensure no warnings remain before submitting PRs

Applied to files:

  • packages/lib/tsconfig.lint.json
  • packages/lib/.eslintrc.cjs
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Use Prisma for database access in apps/web

Applied to files:

  • apps/web/prisma/schema.prisma
🧬 Code graph analysis (15)
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (2)
packages/lib/src/webhook/webhook-events.ts (5)
  • WebhookEvents (36-40)
  • WebhookEventType (42-42)
  • ContactEvents (1-5)
  • DomainEvents (9-14)
  • EmailEvents (18-32)
apps/web/src/store/upgradeModalStore.ts (1)
  • useUpgradeModalStore (13-20)
apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx (3)
packages/ui/src/toaster.tsx (1)
  • toast (31-31)
apps/web/src/components/DeleteResource.tsx (1)
  • DeleteResource (63-199)
packages/ui/src/button.tsx (1)
  • Button (80-80)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/webhook.ts (1)
  • webhookRouter (9-135)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (4)
packages/ui/src/toaster.tsx (1)
  • toast (31-31)
packages/ui/src/button.tsx (1)
  • Button (80-80)
apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (1)
  • WebhookCallStatusBadge (3-41)
apps/web/src/components/code-display.tsx (1)
  • CodeDisplay (15-111)
apps/web/src/server/service/domain-service.ts (3)
packages/lib/src/webhook/webhook-events.ts (2)
  • DomainPayload (86-100)
  • DomainWebhookEventType (16-16)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (84-391)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (4)
packages/ui/src/toaster.tsx (1)
  • toast (31-31)
packages/ui/src/badge.tsx (1)
  • Badge (36-36)
apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx (1)
  • WebhookStatusBadge (3-25)
packages/ui/src/button.tsx (1)
  • Button (80-80)
packages/sdk/src/usesend.ts (2)
packages/sdk/index.ts (1)
  • Webhooks (5-5)
packages/sdk/src/webhooks.ts (1)
  • Webhooks (45-199)
apps/web/src/server/api/routers/webhook.ts (3)
packages/lib/src/webhook/webhook-events.ts (1)
  • WebhookEvents (36-40)
apps/web/src/server/api/trpc.ts (2)
  • createTRPCRouter (82-82)
  • teamProcedure (127-152)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (84-391)
apps/web/src/server/service/ses-hook-parser.ts (3)
apps/web/src/server/service/webhook-service.ts (1)
  • WebhookService (84-391)
packages/lib/src/webhook/webhook-events.ts (4)
  • EmailEventPayloadMap (151-165)
  • EmailStatus (44-58)
  • EmailWebhookEventType (34-34)
  • EmailBasePayload (60-72)
apps/web/src/types/aws-types.ts (3)
  • SesEvent (143-156)
  • SesBounce (30-50)
  • SesClick (91-97)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (10)
packages/ui/src/popover.tsx (3)
  • Popover (36-36)
  • PopoverTrigger (36-36)
  • PopoverContent (36-36)
packages/ui/src/button.tsx (1)
  • Button (80-80)
apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx (1)
  • DeleteWebhook (11-61)
apps/web/src/server/service/webhook-service.ts (2)
  • testWebhook (162-194)
  • updateWebhook (256-293)
packages/ui/src/toaster.tsx (1)
  • toast (31-31)
packages/ui/src/breadcrumb.tsx (5)
  • Breadcrumb (108-108)
  • BreadcrumbList (109-109)
  • BreadcrumbItem (110-110)
  • BreadcrumbLink (111-111)
  • BreadcrumbPage (112-112)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (1)
  • WebhookInfo (13-115)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (1)
  • WebhookCallsTable (28-173)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (1)
  • WebhookCallDetails (14-174)
apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx (1)
  • EditWebhookDialog (66-323)
apps/web/src/components/code-display.tsx (1)
packages/email-editor/src/renderer.tsx (1)
  • code (740-756)
apps/web/src/server/service/contact-queue-service.ts (1)
apps/web/src/server/service/contact-service.ts (1)
  • addOrUpdateContact (19-78)
packages/sdk/src/webhooks.ts (2)
packages/sdk/index.ts (7)
  • WebhookVerificationError (6-6)
  • WEBHOOK_SIGNATURE_HEADER (9-9)
  • WEBHOOK_TIMESTAMP_HEADER (10-10)
  • WEBHOOK_EVENT_HEADER (7-7)
  • WEBHOOK_CALL_HEADER (8-8)
  • Webhooks (5-5)
  • WebhookEventData (14-14)
packages/lib/src/webhook/webhook-events.ts (1)
  • WebhookEventData (194-196)
apps/web/src/server/service/webhook-service.ts (7)
apps/web/src/server/queue/bullmq-context.ts (2)
  • TeamJob (5-5)
  • createWorkerHandler (10-24)
packages/lib/src/webhook/webhook-events.ts (2)
  • WebhookEventType (42-42)
  • WebhookPayloadData (184-185)
apps/web/src/server/queue/queue-constants.ts (2)
  • WEBHOOK_DISPATCH_QUEUE (6-6)
  • DEFAULT_QUEUE_OPTIONS (9-14)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/server/public-api/api-error.ts (1)
  • UnsendApiError (62-75)
apps/web/src/server/service/limit-service.ts (1)
  • LimitService (19-280)
apps/web/src/server/api/routers/limits.ts (1)
apps/web/src/server/service/limit-service.ts (1)
  • LimitService (19-280)
🪛 ast-grep (0.40.3)
apps/web/src/components/code-display.tsx

[warning] 106-106: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
apps/web/src/components/code-display.tsx

[error] 107-107: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🪛 markdownlint-cli2 (0.18.1)
references/webhook-architecture.md

7-7: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


133-133: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


173-173: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


320-320: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


364-364: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (32)
apps/web/src/server/service/contact-queue-service.ts (1)

100-108: LGTM! Team-scoped webhook support correctly implemented.

The changes properly extract and forward teamId to enable team-scoped webhook event emission. The implementation is consistent with the existing job data structure and aligns with the updated addOrUpdateContact signature.

packages/lib/.eslintrc.cjs (1)

1-10: LGTM!

The ESLint configuration is correctly structured and follows the monorepo's conventions. It properly extends the shared library config, uses the TypeScript parser, and correctly references the lint-specific tsconfig file.

apps/web/package.json (1)

42-42: LGTM! Dependency addition is correct.

The addition of @usesend/lib as a workspace dependency aligns with the PR's webhook functionality requirements and follows the monorepo pattern used for other internal packages.

apps/web/src/lib/constants/plans.ts (2)

7-7: LGTM! Webhook limit reason added consistently.

The WEBHOOK enum value follows the existing naming convention and integrates properly with the limit checking system.


21-21: LGTM! Webhook limits configured appropriately.

The webhook limits are properly integrated into the plan structure with sensible values: 1 webhook for FREE tier and unlimited (-1) for BASIC tier. This matches the pattern used for other resource limits.

Also applies to: 30-30, 38-38

packages/ui/src/dropdown-menu.tsx (1)

102-113: LGTM! Checkbox item layout improved.

The refactoring moves the checkbox indicator from the left to the right side using ml-auto, which improves the visual hierarchy. The structural changes maintain the same public API while updating the internal rendering order.

apps/web/src/components/code-display.tsx (2)

107-107: Safe usage of dangerouslySetInnerHTML with Shiki.

The dangerouslySetInnerHTML usage here is safe because Shiki's codeToHtml function properly escapes all code content and only generates safe HTML for syntax highlighting. This is an appropriate use case for this API.


15-111: Component implementation looks solid.

The CodeDisplay component properly handles async highlighting with mount tracking, provides a loading state with unstyled code to preserve layout, and includes copy-to-clipboard functionality. The isMounted pattern in the useEffect correctly prevents state updates after unmount.

references/webhook-architecture.md (1)

1-422: Excellent documentation of the webhook architecture.

This documentation provides comprehensive coverage of the webhook system, including clear diagrams, status flows, payload structures, retry logic, and configuration details. The architecture is well-designed with proper reliability features (locking, retries, auto-disable) and the documentation accurately reflects the implementation.

apps/web/src/server/service/ses-hook-parser.ts (3)

1-11: LGTM!

The webhook-related imports are properly structured and necessary for the new webhook emission functionality.

Also applies to: 33-33


279-302: LGTM! Proper error isolation for webhook emission.

The webhook emission is correctly wrapped in a try-catch block to prevent failures from disrupting the main SES hook processing flow. The error logging includes appropriate context.


310-370: LGTM! Well-structured payload builder with type safety.

The function appropriately constructs type-safe webhook payloads for different email statuses with proper fallbacks and optional chaining.

apps/web/src/server/api/routers/limits.ts (1)

21-22: LGTM!

The webhook limit case follows the established pattern and properly delegates to the limit service.

packages/sdk/src/usesend.ts (2)

6-6: LGTM!

The Webhooks import is correctly added to support the new webhook functionality.


175-195: LGTM! The JSDoc example has been corrected.

The webhooks factory method follows the Stripe pattern and is well-documented. The example now correctly shows passing an options object with headers property, addressing the previous review feedback.

apps/web/src/server/api/root.ts (1)

17-17: LGTM!

The webhook router is properly imported and registered, following the established pattern for other routers in the application.

Also applies to: 40-40

apps/web/src/server/queue/queue-constants.ts (1)

6-7: LGTM!

The new queue constants follow the established naming convention and clearly indicate their purpose for webhook dispatch and cleanup operations.

apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (1)

1-41: LGTM!

The component correctly maps all WebhookCallStatus enum values to appropriate badge colors and labels. The switch statement provides exhaustive coverage with a sensible default fallback.

Minor note: the capitalize class on line 36 is redundant since all labels are already capitalized in the switch cases, but it doesn't cause any issues.

apps/web/src/server/service/domain-service.ts (3)

90-106: LGTM!

The buildDomainPayload helper correctly maps all fields from the Domain model to the DomainPayload type, including proper ISO string conversion for dates and handling of nullable fields.


387-396: LGTM!

The emitDomainEvent helper properly wraps webhook emission with try/catch and structured logging, ensuring that webhook failures don't break domain operations. This is a good defensive pattern.


306-312: LGTM!

The webhook emission logic correctly detects status changes and emits the appropriate event type (domain.verified for successful verification, domain.updated otherwise). Comparing previousStatus against domainWithDns.status ensures events are only emitted when there's an actual change.

apps/web/src/server/api/routers/webhook.ts (1)

1-135: LGTM!

The webhook router is well-structured and follows TRPC best practices:

  • All endpoints are properly scoped by ctx.team.id for multi-tenant isolation
  • Input validation via zod schemas is comprehensive
  • Business logic is cleanly delegated to WebhookService
  • The teamProcedure wrapper ensures proper access control

Based on learnings, using TRPC for client-server communication aligns with the project's conventions.

apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (1)

1-72: LGTM on core component structure and data handling.

The component correctly handles loading states, retry functionality with proper cache invalidation, and payload reconstruction. The responseTimeMs != null check at line 134 properly handles the 0ms edge case.

apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (1)

137-162: LGTM on page setup and auto-selection logic.

The use of React 19's use() hook for async params is correct for Next.js 15. The useEffect for auto-selecting the first call appropriately guards against overwriting user selections.

apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (1)

28-68: LGTM on pagination and filtering implementation.

The cursor-based pagination with proper cursor stack management and filter reset logic is well implemented. The status filter correctly maps to the API query parameters.

apps/web/prisma/schema.prisma (2)

464-506: LGTM on webhook schema design.

The Webhook and WebhookCall models are well-designed with:

  • Appropriate cascade delete behavior for team/webhook relationships
  • SetNull for user deletion preserving webhook history
  • Composite index (teamId, webhookId, status) supporting common query patterns
  • Descending createdAt index for efficient recent-first ordering

450-462: LGTM on webhook status enums.

The WebhookStatus and WebhookCallStatus enums provide clear state management for the webhook lifecycle with appropriate values for active/paused/auto-disabled webhooks and pending/in-progress/delivered/failed/discarded calls.

packages/sdk/src/webhooks.ts (2)

263-272: Timing-safe comparison implementation is correct.

The safeEqual function properly uses timingSafeEqual from crypto module with length check guard. The early return on length mismatch is acceptable since length differences are already observable through timing of other operations.


45-137: LGTM on webhook verification implementation.

The Webhooks class provides a clean API following established patterns (similar to Stripe's SDK). Key security aspects are handled correctly:

  • HMAC-SHA256 signature verification
  • Timestamp validation with configurable tolerance
  • Timing-safe comparison to prevent timing attacks
  • Comprehensive error codes for debugging
apps/web/src/server/service/webhook-service.ts (3)

576-602: LGTM on Redis-based distributed locking.

The lock implementation correctly uses:

  • SET key value PX ttl NX for atomic lock acquisition
  • Lua script for safe release ensuring only the lock owner can release
  • Proper error handling in releaseLock

752-821: LGTM on response capture with streaming support.

The captureResponseText function properly:

  • Checks content-type before attempting to read text
  • Handles Content-Length header for early bailout on oversized responses
  • Uses streaming reader with proper truncation and reader.cancel() to release resources
  • Falls back to response.text() when streaming is unavailable

84-133: LGTM on webhook event emission.

The emit method correctly:

  • Queries for active webhooks matching the event type (or subscribed to all events via empty array)
  • Creates webhook call records before enqueueing to ensure delivery tracking
  • Handles the fan-out pattern for multiple webhooks

Comment on lines +29 to +39
const deliveredCount = last7DaysCalls.filter(
(c) => c.status === WebhookCallStatus.DELIVERED,
).length;
const failedCount = last7DaysCalls.filter(
(c) => c.status === WebhookCallStatus.FAILED,
).length;
const pendingCount = last7DaysCalls.filter(
(c) =>
c.status === WebhookCallStatus.PENDING ||
c.status === WebhookCallStatus.IN_PROGRESS,
).length;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove unused computed values.

deliveredCount, failedCount, and pendingCount are computed but never rendered. This is dead code that wastes cycles on every render.

🔎 Proposed fix
   const calls = callsQuery.data?.items ?? [];
-  const last7DaysCalls = calls.filter(
-    (call) => new Date(call.createdAt) >= sevenDaysAgo,
-  );
-
-  const deliveredCount = last7DaysCalls.filter(
-    (c) => c.status === WebhookCallStatus.DELIVERED,
-  ).length;
-  const failedCount = last7DaysCalls.filter(
-    (c) => c.status === WebhookCallStatus.FAILED,
-  ).length;
-  const pendingCount = last7DaysCalls.filter(
-    (c) =>
-      c.status === WebhookCallStatus.PENDING ||
-      c.status === WebhookCallStatus.IN_PROGRESS,
-  ).length;

If these stats are intended to be displayed, implement the UI; otherwise remove to avoid unnecessary computation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const deliveredCount = last7DaysCalls.filter(
(c) => c.status === WebhookCallStatus.DELIVERED,
).length;
const failedCount = last7DaysCalls.filter(
(c) => c.status === WebhookCallStatus.FAILED,
).length;
const pendingCount = last7DaysCalls.filter(
(c) =>
c.status === WebhookCallStatus.PENDING ||
c.status === WebhookCallStatus.IN_PROGRESS,
).length;
const calls = callsQuery.data?.items ?? [];
const last7DaysCalls = calls.filter(
(call) => new Date(call.createdAt) >= sevenDaysAgo,
);
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx around
lines 29 to 39, the computed variables deliveredCount, failedCount, and
pendingCount are calculated but never used; remove these unused computations to
eliminate wasted work (delete the three const declarations and any now-unused
imports/types), or if the intent was to display these stats, hook them into the
component UI (add the corresponding JSX to render the counts where appropriate)
and ensure they are memoized if needed to avoid repeated computation.

Comment on lines +26 to +39
async function onConfirm(values: z.infer<typeof schema>) {
deleteWebhookMutation.mutate(
{ id: webhook.id },
{
onSuccess: async () => {
await utils.webhook.list.invalidate();
toast.success("Webhook deleted");
},
onError: (error) => {
toast.error(error.message);
},
},
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Async onConfirm should await the mutation.

The onConfirm function is marked async but uses mutate() which doesn't return a Promise. Since DeleteResource awaits onConfirm(), the function returns immediately while the mutation is still pending. Use mutateAsync() to properly chain the async behavior.

🔎 Proposed fix
   async function onConfirm(values: z.infer<typeof schema>) {
-    deleteWebhookMutation.mutate(
+    await deleteWebhookMutation.mutateAsync(
       { id: webhook.id },
-      {
-        onSuccess: async () => {
-          await utils.webhook.list.invalidate();
-          toast.success("Webhook deleted");
-        },
-        onError: (error) => {
-          toast.error(error.message);
-        },
-      },
     );
+    await utils.webhook.list.invalidate();
+    toast.success("Webhook deleted");
   }

Note: Error handling will be managed by the mutation's error state or you can wrap in try/catch if custom error handling is needed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function onConfirm(values: z.infer<typeof schema>) {
deleteWebhookMutation.mutate(
{ id: webhook.id },
{
onSuccess: async () => {
await utils.webhook.list.invalidate();
toast.success("Webhook deleted");
},
onError: (error) => {
toast.error(error.message);
},
},
);
}
async function onConfirm(values: z.infer<typeof schema>) {
await deleteWebhookMutation.mutateAsync(
{ id: webhook.id },
);
await utils.webhook.list.invalidate();
toast.success("Webhook deleted");
}
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx around lines 26 to
39, onConfirm is async but calls deleteWebhookMutation.mutate which is not
promise-based, so the caller awaits prematurely; replace the mutate call with
await deleteWebhookMutation.mutateAsync({ id: webhook.id }) and either keep the
existing onSuccess/onError behavior on the mutation or handle success/error in a
try/catch around the await (e.g., await mutateAsync then await
utils.webhook.list.invalidate() and call toast.success, catch to call
toast.error).

Comment on lines +397 to +428
function emailStatusToEvent(status: EmailStatus): EmailWebhookEventType {
switch (status) {
case EmailStatus.QUEUED:
return "email.queued";
case EmailStatus.SENT:
return "email.sent";
case EmailStatus.DELIVERY_DELAYED:
return "email.delivery_delayed";
case EmailStatus.DELIVERED:
return "email.delivered";
case EmailStatus.BOUNCED:
return "email.bounced";
case EmailStatus.REJECTED:
return "email.rejected";
case EmailStatus.RENDERING_FAILURE:
return "email.rendering_failure";
case EmailStatus.COMPLAINED:
return "email.complained";
case EmailStatus.FAILED:
return "email.failed";
case EmailStatus.CANCELLED:
return "email.cancelled";
case EmailStatus.SUPPRESSED:
return "email.suppressed";
case EmailStatus.OPENED:
return "email.opened";
case EmailStatus.CLICKED:
return "email.clicked";
default:
return "email.queued";
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the EmailStatus enum definition to see all possible values
rg -n "enum EmailStatus|type EmailStatus" --type ts -A 20 -B 2

# Search for emailStatusToEvent usage to understand the context
rg -n "emailStatusToEvent" --type ts -B 3 -A 3

# Check if SCHEDULED is referenced anywhere in the codebase
rg -n "SCHEDULED" --type ts -B 2 -A 2

Repository: usesend/useSend

Length of output: 15706


🏁 Script executed:

# Check what EmailWebhookEventType values are available, especially for SCHEDULED
rg -n "EmailWebhookEventType|type EmailWebhookEventType" --type ts -A 20

# Check if there's any filtering that prevents SCHEDULED emails from reaching webhooks
rg -n "latestStatus.*SCHEDULED|SCHEDULED.*latestStatus" --type ts -B 3 -A 3

Repository: usesend/useSend

Length of output: 14109


🏁 Script executed:

# Get the full webhook-events.ts file to see the EmailEvents definition
cat -n packages/lib/src/webhook/webhook-events.ts | head -40

Repository: usesend/useSend

Length of output: 1189


Add explicit handling for EmailStatus.SCHEDULED in the switch statement.

The switch statement doesn't explicitly handle EmailStatus.SCHEDULED, which falls through to the default case returning "email.queued". Since there is no "email.scheduled" webhook event type available in the EmailEvents array, either filter out SCHEDULED emails before calling this function or throw an error to catch any unexpected status values at runtime.

🤖 Prompt for AI Agents
In apps/web/src/server/service/ses-hook-parser.ts around lines 397-428, the
switch lacks an explicit case for EmailStatus.SCHEDULED so it currently falls to
the default and returns "email.queued"; add an explicit case for
EmailStatus.SCHEDULED and handle it deterministically (either throw a clear
Error indicating SCHEDULED is unsupported here so callers must filter it out, or
map it to a defined event if you intentionally want to treat it as queued), then
update callers or document that SCHEDULED must be filtered before calling this
function.

Comment on lines +518 to +530
const updatedWebhook = await db.webhook.update({
where: { id: call.webhookId },
data: {
consecutiveFailures: {
increment: 1,
},
lastFailureAt: new Date(),
status:
call.webhook.consecutiveFailures + 1 >= WEBHOOK_AUTO_DISABLE_THRESHOLD
? WebhookStatus.AUTO_DISABLED
: call.webhook.status,
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential race condition in auto-disable threshold check.

The threshold check at line 526 uses the pre-update value (call.webhook.consecutiveFailures + 1) while the atomic increment happens in the same update. If multiple webhook calls fail concurrently for the same webhook, each worker reads the same stale consecutiveFailures value, potentially delaying auto-disable.

This is low severity since the webhook will eventually be disabled, but for correctness you could use a transaction with SELECT ... FOR UPDATE or rely on the database-returned value.

🤖 Prompt for AI Agents
In apps/web/src/server/service/webhook-service.ts around lines 518-530, the code
computes the auto-disable status using the stale pre-update consecutiveFailures
value which can race under concurrent failures; wrap the read-and-update in a
transaction that locks the webhook row (SELECT ... FOR UPDATE), read the current
consecutiveFailures, compute newFailures = current + 1 and desired status based
on WEBHOOK_AUTO_DISABLE_THRESHOLD, then perform the update (or perform an atomic
update that returns the updated consecutiveFailures/status and use that returned
value) so the threshold decision is made against the locked/returned value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants