Skip to content

feat: Add recruiter notification engine#240

Merged
JoachimLK merged 13 commits into
mainfrom
feat/email-notification
Jul 22, 2026
Merged

feat: Add recruiter notification engine#240
JoachimLK merged 13 commits into
mainfrom
feat/email-notification

Conversation

@JoachimLK

@JoachimLK JoachimLK commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
  • Implemented a durable outbox pattern for recruiter email notifications (new applicant, candidate reply, AI scoring).
  • Added notificationOutbox and notificationPreference schema tables.
  • Added scheduled workers for instant dispatch and daily digests.
  • Added instance-wide NOTIFICATIONS_ENABLED server kill-switch.
  • Added notifications feature flag for UI visibility.
  • Included email suppression tracking to maintain sender reputation.

Summary

  • What does this PR change?
  • Why is this needed?

PR title must follow Conventional Commits — e.g. feat(jobs): add bulk import or fix: handle null salary. The squash-merged title is what release-please uses to generate the changelog and pick the next version. PRs with non-conventional titles are blocked by CI.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Chore

Validation

  • I tested locally
  • I added/updated relevant documentation
  • I verified multi-tenant scoping and auth behavior for affected API paths

DCO

  • All commits in this PR are signed off (Signed-off-by) via git commit -s

Summary by CodeRabbit

  • New Features
    • Added a Notifications settings page with per-event delivery controls (instant, daily digest, or off), plus dashboard navigation to it.
    • Added recruiter email notifications for application events, candidate replies, and interview responses, including scheduled daily digests.
    • Added application deletion with confirmation, while preserving candidate records.
  • Bug Fixes
    • Excluded quarantined candidates from applications, interviews, dashboards, analytics, and assistant results.
    • Improved notification email reliability with outbox retries, suppression, and webhook-based delivery status tracking.
  • Configuration
    • Added instance-wide notification enable/disable control and expanded cron scheduling for notification dispatch and digests.

- Implemented a durable outbox pattern for recruiter email notifications
  (new applicant, candidate reply, AI scoring).
- Added `notificationOutbox` and `notificationPreference` schema tables.
- Added scheduled workers for instant dispatch and daily digests.
- Added instance-wide `NOTIFICATIONS_ENABLED` server kill-switch.
- Added `notifications` feature flag for UI visibility.
- Included email suppression tracking to maintain sender reputation.
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-240 July 20, 2026 07:40 Destroyed
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@JoachimLK, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 038541c2-5f1c-40c1-8577-9481192a75fd

📥 Commits

Reviewing files that changed from the base of the PR and between d16dde4 and 1a0236e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • app/components/DemoUpsellBanner.vue
  • app/pages/dashboard/applications/index.vue
  • app/pages/dashboard/candidates/index.vue
  • package.json
📝 Walkthrough

Walkthrough

This PR adds recruiter notification preferences, durable outbox delivery, digest scheduling, webhook status processing, and notification settings UI. It also adds application deletion and excludes quarantined candidates from application, analytics, interview, job, and chatbot queries.

Changes

Recruiter notifications

Layer / File(s) Summary
Contracts and persistence
shared/notifications.ts, server/database/..., server/utils/env.ts, .env.example
Defines notification types and modes, adds outbox, preference, and suppression tables, migrations, environment controls, and demo-account suppression.
Preferences UI and API
app/pages/dashboard/settings/notifications.vue, app/composables/useNotificationPreferences.ts, server/api/notification-preferences/*, app/components/Settings*
Adds notification settings navigation, preference loading, validation, persistence, and editable channel modes.
Enqueueing and producers
server/utils/notifications/{recipients,enqueue,interview-response,urls}.ts, server/api/public/..., ee/server/api/webhooks/resend.post.ts
Resolves eligible recipients, creates deduplicated outbox rows, and enqueues application, interview-response, and candidate-reply notifications.
Rendering and delivery
server/utils/notifications/{templates,dispatch,delivery-status}.ts, server/utils/email.ts, server/tasks/*, server/api/admin/notification-dispatch.post.ts, nuxt.config.ts
Adds notification templates, instant and digest dispatch, retries, suppression, provider IDs, webhook status updates, cron tasks, and an authenticated dispatch endpoint.
Notification tests
tests/unit/notification-*.test.ts, tests/unit/demo-account-isolation.test.ts
Covers enqueueing, recipient resolution, template validation, dispatch behavior, delivery statuses, retries, suppression, and demo-account settings.

Application deletion and quarantine visibility

Layer / File(s) Summary
Application deletion flow
server/api/applications/[id].delete.ts, app/composables/useApplication.ts, app/components/ApplicationDetailDrawer.vue, app/pages/dashboard/applications/*
Adds permission-checked application deletion with transactional cleanup, attachment and calendar handling, confirmation dialogs, cache refreshes, and activity recording.
Active-candidate filtering
server/api/applications/*, server/api/jobs/*, server/api/interviews/*, server/api/dashboard/stats.get.ts, server/api/tracking-links/*, ee/server/api/source-tracking/*, server/utils/ai/chatTools.ts
Restricts application, pipeline, interview, analytics, resume, candidate, and chatbot queries to non-quarantined candidates.
Deletion validation
tests/unit/application-deletion.test.ts, e2e/critical-flows/application-deletion.spec.ts
Tests authorization, transactional deletion, interview conflicts, best-effort attachment cleanup, retained candidates, and quarantined application visibility.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EventProducer
  participant enqueueNotification
  participant notification_outbox
  participant runNotificationDispatch
  participant sendNotificationEmail
  participant ResendWebhook
  EventProducer->>enqueueNotification: enqueue typed notification
  enqueueNotification->>notification_outbox: insert deduplicated pending row
  runNotificationDispatch->>notification_outbox: claim due rows or digest groups
  runNotificationDispatch->>sendNotificationEmail: render and send email
  sendNotificationEmail-->>notification_outbox: persist provider message id and sent state
  ResendWebhook->>processNotificationStatusEvent: process delivery status
  processNotificationStatusEvent->>notification_outbox: update status and suppression data
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete: the required template sections are not filled out, and validation, type, and DCO details are missing. Fill in the Summary, Type of change, Validation, and DCO sections with concrete details and mark the applicable checkboxes.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is conventional, concise, and accurately summarizes the main change: a recruiter notification engine.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/email-notification

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.

@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-240 July 20, 2026 07:45 Destroyed
@JoachimLK JoachimLK changed the title Add recruiter notification engine feat: Add recruiter notification engine Jul 20, 2026
@railway-app

railway-app Bot commented Jul 20, 2026

Copy link
Copy Markdown

🚅 Deployed to the reqcore-pr-240 environment in applirank

Service Status Web Updated (UTC)
applirank ✅ Success (View Logs) Web Jul 22, 2026 at 8:36 am

Implement logic to force notification preferences to "off" for the demo
account, preventing unwanted emails during demos. Updates include
enforcing this in API handlers, notification resolution, and the
seeding script.
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-240 July 20, 2026 07:55 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
server/database/migrations/0046_far_hairball.sql (1)

1-6: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Squash these index alterations into the initial migration.

Since both 0045 and 0046 are introduced in the same PR, consider squashing them to maintain a clean migration history and avoid creating and immediately dropping indexes on newly created tables. Consolidating them will also resolve the false-positive warnings from the static analyzer regarding concurrent index operations.

You can achieve this by deleting both migration SQL files (along with their specific metadata records in the meta/ folder) and re-running your migration generation command to produce a single, unified file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/database/migrations/0046_far_hairball.sql` around lines 1 - 6, Squash
the index changes from migration 0046 into the initial migration by deleting
both migration SQL files and their corresponding metadata records under meta/,
then regenerate migrations to produce one unified migration containing the final
index definitions without create-and-drop operations.
tests/unit/notification-delivery-status.test.ts (1)

24-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Digest "leader row" identity isn't actually asserted.

The mock's where: () => Promise.resolve() discards its argument, so this test can't detect a regression in the digest-bucket identity clause (delivery-status.ts lines 57-64) it's named for — it only checks the set() payload.

♻️ Capture and assert the where() clause
       update: () => ({
         set: (values: Record<string, unknown>) => {
           setCalls.push(values)
-          return { where: () => Promise.resolve() }
+          return { where: (clause: unknown) => { whereCalls.push(clause); return Promise.resolve() } }
         },
       }),

Also applies to: 86-95

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/notification-delivery-status.test.ts` around lines 24 - 38, Update
the test mock’s update().set() chain to capture the argument passed to where(),
then assert that the digest leader-row identity clause is included alongside the
existing set() payload assertions. Apply the same capture and assertion to the
additional affected test case, using the symbols and expectations already
present in notification-delivery-status.test.ts.
server/utils/notifications/templates.ts (1)

133-142: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use the Zod 4 top-level helpers here

z.string().url() is deprecated in favor of z.url(), and z.number().finite() is now redundant because z.number() already rejects non-finite values. Swapping them keeps the schema aligned with Zod 4 and avoids deprecated chaining.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/utils/notifications/templates.ts` around lines 133 - 142, Update
basePayloadSchema to use the Zod 4 top-level z.url() helper for applicationUrl,
and simplify analysis_completed’s compositeScore schema by removing the
redundant finite() chain while preserving its number, range, and nullable
constraints.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/api/admin/notification-dispatch.post.ts`:
- Around line 16-39: Update the request-body handling in the notification
dispatch handler around bodySchema and readValidatedBody so an absent body is
treated as an empty object before validation. Preserve the schema defaults,
allowing bodyless cron requests to proceed with digest set to false.

In `@server/api/notification-preferences/index.patch.ts`:
- Around line 30-37: Wrap the per-preference upserts in the
notification-preferences update flow in a database transaction so all entries
succeed or the entire operation rolls back. Update the loop around
db.insert(notificationPreference) to use the repository’s established
transaction API, or replace it with a single batched upsert if supported, while
preserving the existing conflict target and channelMode updates.

---

Nitpick comments:
In `@server/database/migrations/0046_far_hairball.sql`:
- Around line 1-6: Squash the index changes from migration 0046 into the initial
migration by deleting both migration SQL files and their corresponding metadata
records under meta/, then regenerate migrations to produce one unified migration
containing the final index definitions without create-and-drop operations.

In `@server/utils/notifications/templates.ts`:
- Around line 133-142: Update basePayloadSchema to use the Zod 4 top-level
z.url() helper for applicationUrl, and simplify analysis_completed’s
compositeScore schema by removing the redundant finite() chain while preserving
its number, range, and nullable constraints.

In `@tests/unit/notification-delivery-status.test.ts`:
- Around line 24-38: Update the test mock’s update().set() chain to capture the
argument passed to where(), then assert that the digest leader-row identity
clause is included alongside the existing set() payload assertions. Apply the
same capture and assertion to the additional affected test case, using the
symbols and expectations already present in
notification-delivery-status.test.ts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f4217305-2ee2-42ad-9077-d95306c75a79

📥 Commits

Reviewing files that changed from the base of the PR and between a0852f8 and b8c7113.

📒 Files selected for processing (36)
  • .env.example
  • app/components/SettingsMobileNav.vue
  • app/components/SettingsSidebar.vue
  • app/composables/useNotificationPreferences.ts
  • app/pages/dashboard/settings/notifications.vue
  • ee/server/api/webhooks/resend.post.ts
  • nuxt.config.ts
  • server/api/admin/notification-dispatch.post.ts
  • server/api/applications/[id]/analyze.post.ts
  • server/api/notification-preferences/index.get.ts
  • server/api/notification-preferences/index.patch.ts
  • server/api/public/jobs/[slug]/apply.post.ts
  • server/database/migrations/0045_clammy_sleeper.sql
  • server/database/migrations/0046_far_hairball.sql
  • server/database/migrations/meta/0045_snapshot.json
  • server/database/migrations/meta/0046_snapshot.json
  • server/database/migrations/meta/_journal.json
  • server/database/schema/app.ts
  • server/tasks/notification-digest.ts
  • server/tasks/notification-dispatch.ts
  • server/utils/ai/autoScore.ts
  • server/utils/email.ts
  • server/utils/env.ts
  • server/utils/notifications/delivery-status.ts
  • server/utils/notifications/dispatch.ts
  • server/utils/notifications/enqueue.ts
  • server/utils/notifications/recipients.ts
  • server/utils/notifications/templates.ts
  • server/utils/notifications/urls.ts
  • shared/feature-flags.ts
  • shared/notifications.ts
  • tests/unit/notification-delivery-status.test.ts
  • tests/unit/notification-dispatch.test.ts
  • tests/unit/notification-enqueue.test.ts
  • tests/unit/notification-recipients.test.ts
  • tests/unit/notification-templates.test.ts

Comment on lines +16 to +39
const bodySchema = z.object({
digest: z.boolean().optional().default(false),
batchSize: z.number().int().min(1).max(500).optional(),
})

export default defineEventHandler(async (event) => {
const cronSecret = getHeader(event, 'x-cron-secret')
let source: 'cron_endpoint' | 'interactive' = 'interactive'

if (cronSecret && env.CRON_SECRET) {
const supplied = Buffer.from(cronSecret)
const expected = Buffer.from(env.CRON_SECRET)
const valid = supplied.length === expected.length && timingSafeEqual(supplied, expected)
if (!valid) {
throw createError({ statusCode: 403, statusMessage: 'Invalid cron secret' })
}
source = 'cron_endpoint'
}
else {
// Reuse an existing high-trust permission — draining the queue is an admin op.
await requirePermission(event, { organization: ['update'] })
}

const body = await readValidatedBody(event, bodySchema.parse)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

h3 readValidatedBody empty POST body undefined zod object parse

💡 Result:

When using readValidatedBody in H3 with an empty POST body, the underlying readBody function typically returns undefined if no body is present [1]. Because readValidatedBody passes this undefined value directly to your validation schema or function, passing a Zod object schema directly (e.g., readValidatedBody(event, z.object({...}))) will result in a validation failure [2][1]. Zod's .parse() method will throw a ZodError when called with undefined because an empty input does not satisfy the requirements of a Zod object schema [2][3]. To handle empty POST bodies gracefully: 1. Use .safeParse(): Instead of passing the schema directly to readValidatedBody, you can use a custom validation function that handles the case where the body is undefined [3]. 2. Defaulting: If you expect an empty body to be valid, you may need to ensure your Zod schema allows for it or provide a default object during the validation step [4]. Example using a safe validation pattern: ts import { readValidatedBody, createError } from "h3"; import { z } from "zod"; const mySchema = z.object({ name: z.string(), }); app.post("/", async (event) => { const body = await readValidatedBody(event, (data) => { // If body is undefined/empty, decide how to handle it if (data === undefined) { return {}; // Or return a default object that satisfies schema } return mySchema.safeParse(data); }); // Handle the result of the validation if (!body.success) { throw createError({ statusCode: 400, message: "Invalid body" }); } return body.data; }); Keep in mind that readValidatedBody is designed to throw a 400 Validation Error automatically if the provided validation function returns a failing result or throws [3][1]. If you want to customize the error behavior for empty bodies, using the safe parsing pattern above is the recommended approach [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target handler and nearby usages of readValidatedBody in the repo.
git ls-files 'server/api/admin/notification-dispatch.post.ts' 'server/api/**/*.ts' | sed -n '1,120p'
printf '\n--- target file ---\n'
cat -n server/api/admin/notification-dispatch.post.ts | sed -n '1,120p'
printf '\n--- readValidatedBody usages ---\n'
rg -n "readValidatedBody\\(" server -g '*.ts'

Repository: reqcore-inc/reqcore

Length of output: 13118


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find in-repo callers of the notification dispatch endpoint and inspect related admin cron patterns.
printf '%s\n' '--- callers of notification-dispatch ---'
rg -n "notification-dispatch|api/admin/notification-dispatch" .

printf '\n%s\n' '--- retention-cleanup for comparison ---'
cat -n server/api/admin/retention-cleanup.post.ts | sed -n '1,120p'

Repository: reqcore-inc/reqcore

Length of output: 2188


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the notification-dispatch task and how it is scheduled.
printf '%s\n' '--- server/tasks/notification-dispatch.ts ---'
cat -n server/tasks/notification-dispatch.ts | sed -n '1,160p'

printf '\n%s\n' '--- nuxt.config.ts task config around notification-dispatch ---'
sed -n '280,330p' nuxt.config.ts

printf '\n%s\n' '--- docs/env mentions ---'
sed -n '45,75p' .env.example

Repository: reqcore-inc/reqcore

Length of output: 4591


Handle missing request bodies before validating. readValidatedBody(event, bodySchema.parse) will reject an empty cron POST because z.object(...).parse(undefined) throws, so bodyless calls to /api/admin/notification-dispatch fail before dispatch starts. Default undefined to {} or wrap the schema so the optional digest path still works.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/admin/notification-dispatch.post.ts` around lines 16 - 39, Update
the request-body handling in the notification dispatch handler around bodySchema
and readValidatedBody so an absent body is treated as an empty object before
validation. Preserve the schema defaults, allowing bodyless cron requests to
proceed with digest set to false.

Comment on lines +30 to +37
for (const pref of preferences) {
await db.insert(notificationPreference)
.values({ userId, organizationId: orgId, type: pref.type, channelMode: pref.channelMode })
.onConflictDoUpdate({
target: [notificationPreference.userId, notificationPreference.organizationId, notificationPreference.type],
set: { channelMode: pref.channelMode, updatedAt: new Date() },
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Wrap the per-preference upserts in a transaction (or batch them).

Each preference is upserted in its own statement with no transaction wrapping. A failure mid-loop leaves the user with a partially-applied set of preferences and no rollback.

♻️ Proposed fix: single batched upsert
-  for (const pref of preferences) {
-    await db.insert(notificationPreference)
-      .values({ userId, organizationId: orgId, type: pref.type, channelMode: pref.channelMode })
-      .onConflictDoUpdate({
-        target: [notificationPreference.userId, notificationPreference.organizationId, notificationPreference.type],
-        set: { channelMode: pref.channelMode, updatedAt: new Date() },
-      })
-  }
+  await db.insert(notificationPreference)
+    .values(preferences.map(pref => ({
+      userId, organizationId: orgId, type: pref.type, channelMode: pref.channelMode,
+    })))
+    .onConflictDoUpdate({
+      target: [notificationPreference.userId, notificationPreference.organizationId, notificationPreference.type],
+      set: { channelMode: sql`excluded.channel_mode`, updatedAt: new Date() },
+    })
📝 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
for (const pref of preferences) {
await db.insert(notificationPreference)
.values({ userId, organizationId: orgId, type: pref.type, channelMode: pref.channelMode })
.onConflictDoUpdate({
target: [notificationPreference.userId, notificationPreference.organizationId, notificationPreference.type],
set: { channelMode: pref.channelMode, updatedAt: new Date() },
})
}
await db.insert(notificationPreference)
.values(preferences.map(pref => ({
userId, organizationId: orgId, type: pref.type, channelMode: pref.channelMode,
})))
.onConflictDoUpdate({
target: [notificationPreference.userId, notificationPreference.organizationId, notificationPreference.type],
set: { channelMode: sql`excluded.channel_mode`, updatedAt: new Date() },
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/notification-preferences/index.patch.ts` around lines 30 - 37,
Wrap the per-preference upserts in the notification-preferences update flow in a
database transaction so all entries succeed or the entire operation rolls back.
Update the loop around db.insert(notificationPreference) to use the repository’s
established transaction API, or replace it with a single batched upsert if
supported, while preserving the existing conflict target and channelMode
updates.

Prevents the migration from running if the schema changes already exist
in the Drizzle ledger from migrations 0045 and 0046.
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-240 July 20, 2026 08:05 Destroyed
- Add `interview_response` notification type
- Remove `analysis_completed` notification type and associated outbox
  entries
- Implement notification for interview status changes
  (accepted/declined/tentative)
- Add database migration to update `notification_type` enum and cleanup
  legacy data
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-240 July 20, 2026 11:56 Destroyed
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-240 July 20, 2026 12:11 Destroyed
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-240 July 20, 2026 12:17 Destroyed
/** Durable notification outbox dispatch with leases, retries, and digest grouping. */
import { and, asc, eq, gt, inArray, lte, sql } from 'drizzle-orm'
import { emailSuppression, notificationOutbox } from '../../database/schema/app'
import { isServerFeatureEnabled } from '../featureFlags'
Implement a DELETE endpoint for applications, allowing users to remove
specific applications and their associated data (comments, properties,
and attachments). Added UI triggers, backend logic, and validation to
ensure scheduled interviews are cancelled before deletion.
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-240 July 20, 2026 12:51 Destroyed
Move the S3 object deletion logic after the database transaction to
prevent
orphaned database records if the transaction fails.
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-240 July 20, 2026 13:08 Destroyed
Decouple notification logic from database transactions to ensure that
notification delivery failures cannot roll back primary business
operations. Updated all notification producers to enqueue events after
the
DB transaction commits.
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-240 July 20, 2026 13:31 Destroyed
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-240 July 22, 2026 08:15 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
server/utils/notifications/enqueue.ts (2)

38-55: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Await the outbox enqueue at every producer.

The supplied call sites invoke enqueueNotification(...) without await, while this function yields during recipient resolution at Line 38. The request can finish before Line 54 inserts the row, and process shutdown can drop the notification. Await those producers or use a lifecycle-safe background queue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/utils/notifications/enqueue.ts` around lines 38 - 55, Update every
producer that calls enqueueNotification to await its returned promise, ensuring
recipient resolution and the notificationOutbox insert complete before the
request or job finishes. Locate all enqueueNotification call sites and preserve
their existing control flow; do not leave fire-and-forget invocations unless
they are routed through a lifecycle-safe background queue.

57-65: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not swallow enqueue failures as successful no-ops.

If recipient resolution or the insert fails, Lines 57-65 only log the error. Because the primary transaction has already committed, no outbox row or retry remains, so the notification is permanently lost. Add a bounded retry or durable recovery path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/utils/notifications/enqueue.ts` around lines 57 - 65, Update the catch
path in the notification enqueue flow to provide bounded retries or another
durable recovery mechanism when recipient resolution or insertion fails, rather
than only logging via logWarn. Ensure the recovery preserves the notification
payload and dedupe_key, prevents unbounded retrying, and leaves a durable
outbox/retry record after the primary transaction has committed.
🧹 Nitpick comments (1)
tests/unit/demo-account-isolation.test.ts (1)

31-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the handler instead of matching its source text.

Lines 32-35 only search for literal strings, so the test can pass with unreachable or miswired gating and fail on harmless formatting changes. Invoke the GET handler with a demo session and assert { completed: true } at runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/demo-account-isolation.test.ts` around lines 31 - 36, Update the
test case around the onboarding survey GET handler to invoke its exported
handler with a demo-account session instead of matching source text via read().
Assert the runtime response equals { completed: true }, ensuring the
demo-account gating is exercised through the actual handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@server/utils/notifications/enqueue.ts`:
- Around line 38-55: Update every producer that calls enqueueNotification to
await its returned promise, ensuring recipient resolution and the
notificationOutbox insert complete before the request or job finishes. Locate
all enqueueNotification call sites and preserve their existing control flow; do
not leave fire-and-forget invocations unless they are routed through a
lifecycle-safe background queue.
- Around line 57-65: Update the catch path in the notification enqueue flow to
provide bounded retries or another durable recovery mechanism when recipient
resolution or insertion fails, rather than only logging via logWarn. Ensure the
recovery preserves the notification payload and dedupe_key, prevents unbounded
retrying, and leaves a durable outbox/retry record after the primary transaction
has committed.

---

Nitpick comments:
In `@tests/unit/demo-account-isolation.test.ts`:
- Around line 31-36: Update the test case around the onboarding survey GET
handler to invoke its exported handler with a demo-account session instead of
matching source text via read(). Assert the runtime response equals { completed:
true }, ensuring the demo-account gating is exercised through the actual
handler.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9fcabac1-9e53-4fbd-97ab-dd8419d97fbb

📥 Commits

Reviewing files that changed from the base of the PR and between dcc38e9 and d16dde4.

📒 Files selected for processing (8)
  • ee/server/api/webhooks/resend.post.ts
  • server/api/onboarding-survey/index.get.ts
  • server/api/public/interviews/respond.post.ts
  • server/api/public/jobs/[slug]/apply.post.ts
  • server/utils/notifications/enqueue.ts
  • server/utils/notifications/interview-response.ts
  • tests/unit/demo-account-isolation.test.ts
  • tests/unit/notification-enqueue.test.ts
💤 Files with no reviewable changes (1)
  • server/utils/notifications/interview-response.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • server/api/public/jobs/[slug]/apply.post.ts
  • tests/unit/notification-enqueue.test.ts
  • ee/server/api/webhooks/resend.post.ts

- Add `ring-1` and `rounded-t-xl` to DemoUpsellBanner
- Add `cursor-pointer` to table action buttons
- Update `axios` and `tar` versions in overrides
- Regenerate package-lock.json with peer dependency updates
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-240 July 22, 2026 08:25 Destroyed
Refine dependency definitions and peer dependency flags to ensure
consistent installation state.
@railway-app
railway-app Bot temporarily deployed to applirank / reqcore-pr-240 July 22, 2026 08:33 Destroyed
@JoachimLK
JoachimLK merged commit cb20514 into main Jul 22, 2026
10 checks passed
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.

1 participant