Skip to content

Partner account verification via Veriff#3614

Open
devkiran wants to merge 37 commits intomainfrom
veriff
Open

Partner account verification via Veriff#3614
devkiran wants to merge 37 commits intomainfrom
veriff

Conversation

@devkiran
Copy link
Copy Markdown
Collaborator

@devkiran devkiran commented Mar 19, 2026

Summary by CodeRabbit

  • New Features

    • Partner identity verification via Veriff: dashboard identity-verification panel, action button to start/resubmit verification, status badges, and "Identity verified" date display.
    • Real-time verification handling with success/failure emails and updated partner status shown in the dashboard.
    • New verified badge and Veriff branding in relevant UI components.
  • Chores

    • Added Veriff environment placeholders for API key and webhook secret.

- Add IdentityVerificationStatus enum and fields to Partner model
- Create Veriff better-fetch client, schemas, and session helpers
- Add webhook handler for Veriff decision callbacks with HMAC verification
- Create server action for starting identity verification
- Add IdentityVerificationCard component to partner profile page
- Install @veriff/incontext-sdk for in-modal verification flow
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 19, 2026

📝 Walkthrough

Walkthrough

Adds partner identity verification via Veriff: DB enum/fields, Prisma index, Zod schemas, server action to create Veriff sessions, webhook route + handlers, UI component and badge, email templates, icon assets, package dependency, and a renamed rate-limit flag (skipAuthThrottlingshouldApplyRateLimit).

Changes

Cohort / File(s) Summary
Environment & Config
apps/web/.env.example, apps/web/lib/api/environment.ts
Added VERIFF_API_KEY and VERIFF_WEBHOOK_SECRET placeholders; replaced skipAuthThrottling export with shouldApplyRateLimit.
Database Schema
packages/prisma/schema/partner.prisma
Added IdentityVerificationStatus enum and partner fields: identityVerificationStatus, identityVerificationAttemptCount, identityVerificationDeclineReason, identityVerifiedAt, Veriff session fields (veriffSessionExpiresAt, veriffSessionUrl, veriffSessionId), veriffIdentityHash, and @@index(identityVerifiedAt).
Veriff API Validation
apps/web/lib/veriff/schema.ts
New Zod schemas for Veriff session create request/response, session webhook events, and decision webhook events plus union event schema.
Zod Partner Schemas & Constants
apps/web/lib/zod/schemas/partners.ts, apps/web/lib/zod/schemas/partner-network.ts
Extended PartnerSchema and downstream picks with identity verification fields; added constants including MAX_PARTNER_IDENTITY_VERIFICATION_ATTEMPTS.
Server Actions & Rate-limit Flag
apps/web/lib/actions/partners/start-identity-verification.ts, apps/web/lib/actions/create-user-account.ts, apps/web/lib/actions/check-account-exists.ts, apps/web/lib/api/environment.ts, apps/web/lib/auth/options.ts
Added startIdentityVerificationAction and helper to create Veriff sessions; swapped usage to shouldApplyRateLimit and inverted conditional logic where rate limiting is applied.
Webhook Endpoint & Handlers
apps/web/app/api/veriff/webhook/route.ts, apps/web/app/api/veriff/webhook/handle-session-event.ts, apps/web/app/api/veriff/webhook/handle-decision-event.ts
Added POST webhook route with HMAC and x-auth-client verification; handleSessionEvent updates partner session/status; handleDecisionEvent computes identity hash, checks duplicates/country mismatch, determines effective status/reason, updates partner conditionally, increments attempts, and sends idempotent emails.
Partner Profile UI
apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/identity-verification-section.tsx, apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx, apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/page-client.tsx
Added IdentityVerificationSection component (client) with Veriff iframe integration and status-driven UI; integrated into profile form; minor import spacing change.
API & Display
apps/web/app/(ee)/api/network/partners/route.ts, apps/web/ui/partners/partner-info-cards.tsx, apps/web/lib/auth/partner.ts
Extended network partner response mapping to include identityVerificationStatus and identityVerifiedAt; show VerifiedBadge when approved; excluded veriffIdentityHash from partner relation selects.
Email Templates
packages/email/src/templates/partner-identity-verified.tsx, packages/email/src/templates/partner-identity-verification-failed.tsx
Added success and failure email templates (failure interpolates decline reason); templates export default React Email components.
UI Icons & Exports
packages/ui/src/icons/verified-badge.tsx, packages/ui/src/icons/veriff.tsx, packages/ui/src/icons/index.tsx
Added VerifiedBadge and Veriff SVG components and re-exported them from icons barrel.
Minor UI Changes
apps/web/app/(ee)/app.dub.co/embed/referrals/bounties/detail.tsx, apps/web/app/(ee)/app.dub.co/embed/referrals/bounties/submission-detail.tsx
Small className adjustments on back buttons (styling only).
Dependencies
apps/web/package.json
Added runtime dependency @veriff/incontext-sdk and ensured @playwright/test in devDependencies.

Sequence Diagram(s)

sequenceDiagram
    participant User as Partner (Browser)
    participant UI as IdentityVerificationSection
    participant Action as startIdentityVerificationAction
    participant Veriff as Veriff API
    participant DB as Database
    participant SDK as Veriff Iframe

    User->>UI: Click "Start verification"
    UI->>Action: startIdentityVerificationAction(partner)
    Action->>DB: Query partner & existing session
    alt Session valid
        Action-->>UI: Return existing sessionUrl
    else Create session
        Action->>Veriff: POST /v1/sessions (X-AUTH-CLIENT)
        Veriff-->>Action: sessionUrl, sessionId
        Action->>DB: Persist sessionId, sessionUrl, expiresAt
        Action-->>UI: Return sessionUrl
    end
    UI->>SDK: import `@veriff/incontext-sdk`, createFrame(sessionUrl)
    SDK-->>UI: Frame initialized
    User->>SDK: Submit documents
    SDK->>Veriff: Submit verification
    Veriff-->>SDK: MESSAGES.FINISHED
    UI->>UI: Show toast, call mutate()
Loading
sequenceDiagram
    participant Veriff as Veriff Service
    participant Webhook as /api/veriff/webhook
    participant Handler as handleDecisionEvent
    participant DB as Database
    participant Email as Email Service

    Veriff->>Webhook: POST decision event (HMAC signed)
    Webhook->>Webhook: Verify HMAC-SHA256 signature & x-auth-client
    alt Signature/Client invalid
        Webhook-->>Veriff: 400/401
    else Signature valid
        Webhook->>Handler: route decision event
        Handler->>DB: Find partner by veriffSessionId
        alt Partner not found or already verified
            Handler-->>Webhook: exit (no update)
        else
            Handler->>Handler: compute identity hash
            Handler->>DB: check duplicate identity / country mismatch
            alt Checks pass (approve)
                Handler->>DB: update partner (identityVerifiedAt, hash, clear session)
                Handler->>Email: send verified email (Idempotency-Key)
            else Checks fail (decline)
                Handler->>DB: update partner (decline reason, clear session, increment attempts)
                Handler->>Email: send failed email (Idempotency-Key)
            end
            Handler-->>Webhook: 200
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • pepeladeira
  • steven-tey

Poem

🐰 I hopped to add a Veriff light,
Sessions spun and badges bright,
Webhooks thump and emails sing,
Partners verified — fluff and spring! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 'Partner account verification via Veriff' accurately and concisely describes the main change: integrating Veriff-based identity verification for partner accounts across multiple components and APIs.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch veriff

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.

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 19, 2026

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

Project Deployment Actions Updated (UTC)
dub Error Error Mar 27, 2026 8:06am

Request Review

@devkiran devkiran marked this pull request as ready for review March 26, 2026 09:18
Copy link
Copy Markdown
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.

🧹 Nitpick comments (2)
apps/web/lib/zod/schemas/partners.ts (1)

580-581: Nitpick: Redundant field picks.

EnrolledPartnerSchemaExtended extends EnrolledPartnerSchema, which already picks identityVerificationStatus and identityVerifiedAt (lines 480-481). Picking them again here from PartnerSchema is redundant since they're the same fields from the same source. This is harmless but could be cleaned up for clarity.

♻️ Suggested fix
   .extend(
     PartnerSchema.pick({
       monthlyTraffic: true,
       industryInterests: true,
       preferredEarningStructures: true,
       salesChannels: true,
-      identityVerificationStatus: true,
-      identityVerifiedAt: true,
     }).shape,
   )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/zod/schemas/partners.ts` around lines 580 - 581,
EnrolledPartnerSchemaExtended currently re-picks identityVerificationStatus and
identityVerifiedAt from PartnerSchema even though they are already included via
EnrolledPartnerSchema; remove the redundant picks from
EnrolledPartnerSchemaExtended so those two fields are only supplied by
EnrolledPartnerSchema (locate the EnrolledPartnerSchemaExtended definition and
delete the identityVerificationStatus and identityVerifiedAt entries that
duplicate EnrolledPartnerSchema).
apps/web/.env.example (1)

193-193: Optional: Alphabetical key ordering.

The linter flags that E2E_PARTNER_PASSWORD should come before PLAYWRIGHT_BASE_URL alphabetically. However, the current logical grouping (base URL first, then credentials) makes semantic sense for the E2E section, so this can be safely ignored if preferred.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/.env.example` at line 193, The linter wants ENV keys alphabetized;
to satisfy it, move the E2E_PARTNER_PASSWORD entry so it appears before
PLAYWRIGHT_BASE_URL (i.e., place the line "E2E_PARTNER_PASSWORD=" above the
"PLAYWRIGHT_BASE_URL" key) in apps/web/.env.example, or if you prefer to keep
the semantic grouping, add a one-line linter-ignore directive/comment recognized
by your linter next to the E2E keys and ensure the symbol E2E_PARTNER_PASSWORD
remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/.env.example`:
- Line 193: The linter wants ENV keys alphabetized; to satisfy it, move the
E2E_PARTNER_PASSWORD entry so it appears before PLAYWRIGHT_BASE_URL (i.e., place
the line "E2E_PARTNER_PASSWORD=" above the "PLAYWRIGHT_BASE_URL" key) in
apps/web/.env.example, or if you prefer to keep the semantic grouping, add a
one-line linter-ignore directive/comment recognized by your linter next to the
E2E keys and ensure the symbol E2E_PARTNER_PASSWORD remains unchanged.

In `@apps/web/lib/zod/schemas/partners.ts`:
- Around line 580-581: EnrolledPartnerSchemaExtended currently re-picks
identityVerificationStatus and identityVerifiedAt from PartnerSchema even though
they are already included via EnrolledPartnerSchema; remove the redundant picks
from EnrolledPartnerSchemaExtended so those two fields are only supplied by
EnrolledPartnerSchema (locate the EnrolledPartnerSchemaExtended definition and
delete the identityVerificationStatus and identityVerifiedAt entries that
duplicate EnrolledPartnerSchema).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f0da3d4a-1845-4465-9758-532b17f6ae51

📥 Commits

Reviewing files that changed from the base of the PR and between c229abe and d2a4208.

📒 Files selected for processing (2)
  • apps/web/.env.example
  • apps/web/lib/zod/schemas/partners.ts

Copy link
Copy Markdown
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: 3

♻️ Duplicate comments (1)
apps/web/app/api/veriff/webhook/handle-decision-event.ts (1)

64-67: ⚠️ Potential issue | 🔴 Critical

Make duplicate-identity approval authoritative at write time.

checkDuplicateIdentity() is still a read-before-write check. Because veriffIdentityHash is not unique in packages/prisma/schema/partner.prisma, two concurrent "approved" webhooks can both pass the lookup and both persist the same identity. Keep the pre-check for the friendly decline path if you want, but the approval write needs a DB-backed uniqueness guarantee and a loser path when that constraint is hit.

Also applies to: 123-129, 178-190

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/veriff/webhook/handle-decision-event.ts` around lines 64 -
67, The current checkDuplicateIdentity() is only a read-before-write and racey;
make the approval write authoritative by enforcing a DB uniqueness constraint
and handling constraint violations at write time: add a unique constraint for
veriffIdentityHash (or the appropriate composite key) in the Prisma schema and
run a migration, then wrap the approval create/upsert call (the code path that
persists an "approved" Veriff identity) in a try/catch and specifically handle
PrismaClientKnownRequestError P2002 (unique constraint) to treat the conflict as
a duplicate (log and transition to the loser/decline path). You may keep the
existing checkDuplicateIdentity() for a friendly early-decline branch, but the
final decision must be based on the DB-write outcome and its error handling.
🧹 Nitpick comments (1)
packages/ui/src/icons/verified-badge.tsx (1)

3-86: Consider using unique IDs to prevent potential collisions.

The hardcoded IDs verifiedBadgeGradient and verifiedBadgeFilter will be duplicated if multiple instances of this component render on the same page, which is invalid HTML and may cause gradient/filter references to fail in some browsers.

Since you're on React 19, you can use the useId hook to generate unique IDs per instance.

♻️ Proposed fix using useId
-import { SVGProps } from "react";
+import { SVGProps, useId } from "react";

 export function VerifiedBadge(props: SVGProps<SVGSVGElement>) {
+  const id = useId();
+  const gradientId = `verifiedBadgeGradient-${id}`;
+  const filterId = `verifiedBadgeFilter-${id}`;
+
   return (
     <svg
       width={32}
       height={32}
       viewBox="0 0 32 32"
       fill="none"
       xmlns="http://www.w3.org/2000/svg"
       {...props}
     >
-      <g filter="url(`#verifiedBadgeFilter`)">
+      <g filter={`url(#${filterId})`}>
         <path
           d="M26.6745 7.91113L18.7995 5.37618C18.276 5.20719 17.7225 5.20869 17.2005 5.37618L9.324 7.91113C8.232 8.26271 7.5 9.27367 7.5 10.4265V20.2645C7.5 25.5577 14.919 28.3809 17.19 29.1202C17.4555 29.2062 17.727 29.25 18 29.25C18.273 29.25 18.543 29.2078 18.807 29.1217C21.081 28.3824 28.5 25.5592 28.5 20.266V10.4265C28.5 9.27367 27.7665 8.26271 26.6745 7.91113Z"
-          fill="url(`#verifiedBadgeGradient`)"
+          fill={`url(#${gradientId})`}
         />
       </g>
       <defs>
         <linearGradient
-          id="verifiedBadgeGradient"
+          id={gradientId}
           ...
         >
           ...
         </linearGradient>
         <filter
-          id="verifiedBadgeFilter"
+          id={filterId}
           ...
         >
           ...
         </filter>
       </defs>
     </svg>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/icons/verified-badge.tsx` around lines 3 - 86, The SVG uses
hardcoded IDs "verifiedBadgeGradient" and "verifiedBadgeFilter" which will
collide when multiple VerifiedBadge components mount; update VerifiedBadge to
call React's useId() (or accept an id prop) to generate a unique base ID, then
replace the literal ids and their references (id="verifiedBadgeGradient" ->
id={`${uid}-verifiedBadgeGradient`} and filter="url(`#verifiedBadgeFilter`)" ->
filter={`url(#${uid}-verifiedBadgeFilter)`}, and similarly for all href/uri
references) so each instance has unique gradient/filter IDs; keep the same
element names and props (VerifiedBadge, the <linearGradient> and <filter>
elements and their references) to locate the replacements.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/app/api/veriff/webhook/handle-decision-event.ts`:
- Around line 200-208: The checkCountryMismatch function currently returns true
on match; change its logic so it returns true only when countries do not match:
compute veriffCountry as now from verification.document?.country ||
verification.person?.nationality, and if either veriffCountry or partner.country
is missing treat that as a mismatch (return true); otherwise return
partner.country.toUpperCase() !== veriffCountry. Update the return expression in
checkCountryMismatch to use !== instead of === and flip the missing-data return
value.
- Around line 131-160: The current branch sends the generic "declined" email for
every non-approved status; update the logic around effectiveStatus in the
handler so that if effectiveStatus === "review" you skip sending any email, and
for other non-approved statuses (e.g., "expired", "abandoned",
"resubmission_requested") call sendEmail with status-specific
templates/messaging instead of PartnerIdentityVerificationFailed for all cases.
Use the same identifiers (attemptId, partner, sendEmail,
PartnerIdentityVerified, PartnerIdentityVerificationFailed, effectiveStatus,
effectiveReason) to locate the code, ensure the Idempotency-Key still includes
attemptId with a status-specific suffix, and make
identityVerificationDeclineReason safe by defaulting to a sensible fallback
string only for statuses that actually have a reason to display.
- Line 121: Remove the raw dump of the verification payload
(console.log(toUpdate)); instead log only non-sensitive metadata such as
partner.id and effectiveStatus. Locate the console.log(toUpdate) call (in the
webhook handler where the toUpdate object is built) and replace it with a single
log entry that references only safe fields (e.g., partner.id and
toUpdate.effectiveStatus) using the existing logger; do not include
veriffIdentityHash, decline reasons, or other identity-derived data in logs.

---

Duplicate comments:
In `@apps/web/app/api/veriff/webhook/handle-decision-event.ts`:
- Around line 64-67: The current checkDuplicateIdentity() is only a
read-before-write and racey; make the approval write authoritative by enforcing
a DB uniqueness constraint and handling constraint violations at write time: add
a unique constraint for veriffIdentityHash (or the appropriate composite key) in
the Prisma schema and run a migration, then wrap the approval create/upsert call
(the code path that persists an "approved" Veriff identity) in a try/catch and
specifically handle PrismaClientKnownRequestError P2002 (unique constraint) to
treat the conflict as a duplicate (log and transition to the loser/decline
path). You may keep the existing checkDuplicateIdentity() for a friendly
early-decline branch, but the final decision must be based on the DB-write
outcome and its error handling.

---

Nitpick comments:
In `@packages/ui/src/icons/verified-badge.tsx`:
- Around line 3-86: The SVG uses hardcoded IDs "verifiedBadgeGradient" and
"verifiedBadgeFilter" which will collide when multiple VerifiedBadge components
mount; update VerifiedBadge to call React's useId() (or accept an id prop) to
generate a unique base ID, then replace the literal ids and their references
(id="verifiedBadgeGradient" -> id={`${uid}-verifiedBadgeGradient`} and
filter="url(`#verifiedBadgeFilter`)" ->
filter={`url(#${uid}-verifiedBadgeFilter)`}, and similarly for all href/uri
references) so each instance has unique gradient/filter IDs; keep the same
element names and props (VerifiedBadge, the <linearGradient> and <filter>
elements and their references) to locate the replacements.
🪄 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: 5b68f317-842c-4df4-8d9e-01e286d8d08b

📥 Commits

Reviewing files that changed from the base of the PR and between d2a4208 and 7a63837.

📒 Files selected for processing (11)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/identity-verification-section.tsx
  • apps/web/app/api/veriff/webhook/handle-decision-event.ts
  • apps/web/app/api/veriff/webhook/handle-session-event.ts
  • apps/web/app/api/veriff/webhook/route.ts
  • apps/web/lib/actions/partners/start-identity-verification.ts
  • apps/web/lib/veriff/schema.ts
  • apps/web/ui/partners/partner-info-cards.tsx
  • packages/email/src/templates/partner-identity-verification-failed.tsx
  • packages/prisma/schema/partner.prisma
  • packages/ui/src/icons/index.tsx
  • packages/ui/src/icons/verified-badge.tsx
✅ Files skipped from review due to trivial changes (3)
  • packages/ui/src/icons/index.tsx
  • apps/web/ui/partners/partner-info-cards.tsx
  • apps/web/lib/veriff/schema.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/web/app/api/veriff/webhook/route.ts
  • apps/web/app/api/veriff/webhook/handle-session-event.ts
  • apps/web/lib/actions/partners/start-identity-verification.ts
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/identity-verification-section.tsx
  • packages/prisma/schema/partner.prisma

Copy link
Copy Markdown
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/app/api/veriff/webhook/handle-decision-event.ts (2)

129-158: ⚠️ Potential issue | 🟠 Major

Use status-specific messaging for non-approved outcomes.

The else branch at Lines 144-158 also runs for review, expired, abandoned, and resubmission_requested. That sends a "declined" email for review, and effectiveReason || "" can feed empty copy into PartnerIdentityVerificationFailed, which expects an actual decline reason. Skip email for review and branch the remaining statuses to templates/subjects that match the stored status.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/veriff/webhook/handle-decision-event.ts` around lines 129 -
158, The current branch sends a "declined" email for any non-approved status;
update the logic around partner.email, effectiveStatus, attemptId and sendEmail
so that: 1) if effectiveStatus === "approved" send PartnerIdentityVerified as
before; 2) if effectiveStatus === "review" do not send an email; 3) for explicit
failure statuses (e.g., "declined", "expired", "abandoned",
"resubmission_requested") send status-specific emails using distinct subjects
and template components (e.g., PartnerIdentityVerificationFailed for true
declines, a separate ResubmissionRequested template for
"resubmission_requested", etc.); and 4) ensure PartnerIdentityVerificationFailed
always receives a non-empty decline reason (use effectiveReason or a default
like "unspecified" rather than an empty string). Locate and update the sendEmail
calls and the use of PartnerIdentityVerificationFailed/PartnerIdentityVerified
to implement this branching.

64-67: ⚠️ Potential issue | 🔴 Critical

Make duplicate-identity approval atomic.

Lines 64-67 still do a read-before-write duplicate check, but packages/prisma/schema/partner.prisma:47-56 still leaves veriffIdentityHash non-unique. Two concurrent "approved" webhooks with the same hash can both pass the pre-check and both succeed at Lines 121-127. Please make the write authoritative with a DB uniqueness guarantee and handle the loser path when that constraint trips.

Also applies to: 121-127, 176-188

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/veriff/webhook/handle-decision-event.ts` around lines 64 -
67, The current read-before-write duplicate check (checkDuplicateIdentity) is
racy because veriffIdentityHash is not unique; add a DB-level uniqueness
constraint for veriffIdentityHash in the Prisma Partner model so the database is
authoritative, then change the write paths in the webhook handler (the approval
write at the sections around lines handling the approved webhook — the code that
writes veriffIdentityHash at the 121-127 and 176-188 hotspots) to perform the
write and handle a uniqueness violation instead of only relying on the
pre-check: attempt the update/create inside a try/catch, catch
PrismaClientKnownRequestError with code 'P2002' (unique constraint violation)
and treat that as the loser path (mark as duplicate/ignore/return early and
log), and remove the unsafe read-before-write reliance so the DB constraint
enforces atomicity. Ensure checkDuplicateIdentity can remain for early-return
optimization but the authoritative decision comes from catching the DB
unique-constraint error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/app/api/veriff/webhook/handle-decision-event.ts`:
- Around line 37-48: The partner lookup using prisma.partner.findUnique by
veriffSessionId makes retries fail because veriffSessionId is cleared before
sendEmail() runs; change the flow so that a retryable key remains until side
effects succeed by either (a) resolving partner by an immutable identifier
(e.g., partner.id) stored earlier and use that for subsequent lookups instead of
veriffSessionId, or (b) move email delivery into an outbox/background job before
clearing veriffSessionId and only clear the veriffSessionId after the outbox job
is persisted; apply this fix to all places where prisma.partner.findUnique is
called with veriffSessionId and where sendEmail() is invoked so retries will
find the partner and not drop notifications.
- Around line 61-84: When handling an approved Veriff decision in the
effectiveStatus === "approved" branch, do not treat a missing or non-computable
identity hash as a pass; if computeIdentityHash(verification) returns
null/undefined, abort the automatic approval flow and mark the record as
declined or flagged for manual review instead of setting
toUpdate.veriffIdentityHash = null and proceeding. Update the logic around
computeIdentityHash, checkDuplicateIdentity, and checkCountryMismatch so you
only call checkDuplicateIdentity when a non-null identityHash exists, and ensure
the branches that currently set veriffIdentityHash/veriffSession* (the same
pattern repeated elsewhere around computeIdentityHash) implement the same "fail
closed" behavior by rejecting/flagging when the hash cannot be computed.

---

Duplicate comments:
In `@apps/web/app/api/veriff/webhook/handle-decision-event.ts`:
- Around line 129-158: The current branch sends a "declined" email for any
non-approved status; update the logic around partner.email, effectiveStatus,
attemptId and sendEmail so that: 1) if effectiveStatus === "approved" send
PartnerIdentityVerified as before; 2) if effectiveStatus === "review" do not
send an email; 3) for explicit failure statuses (e.g., "declined", "expired",
"abandoned", "resubmission_requested") send status-specific emails using
distinct subjects and template components (e.g.,
PartnerIdentityVerificationFailed for true declines, a separate
ResubmissionRequested template for "resubmission_requested", etc.); and 4)
ensure PartnerIdentityVerificationFailed always receives a non-empty decline
reason (use effectiveReason or a default like "unspecified" rather than an empty
string). Locate and update the sendEmail calls and the use of
PartnerIdentityVerificationFailed/PartnerIdentityVerified to implement this
branching.
- Around line 64-67: The current read-before-write duplicate check
(checkDuplicateIdentity) is racy because veriffIdentityHash is not unique; add a
DB-level uniqueness constraint for veriffIdentityHash in the Prisma Partner
model so the database is authoritative, then change the write paths in the
webhook handler (the approval write at the sections around lines handling the
approved webhook — the code that writes veriffIdentityHash at the 121-127 and
176-188 hotspots) to perform the write and handle a uniqueness violation instead
of only relying on the pre-check: attempt the update/create inside a try/catch,
catch PrismaClientKnownRequestError with code 'P2002' (unique constraint
violation) and treat that as the loser path (mark as duplicate/ignore/return
early and log), and remove the unsafe read-before-write reliance so the DB
constraint enforces atomicity. Ensure checkDuplicateIdentity can remain for
early-return optimization but the authoritative decision comes from catching the
DB unique-constraint error.
🪄 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: f964ea3c-b3d1-49d0-ba2c-95083b47b888

📥 Commits

Reviewing files that changed from the base of the PR and between 72c7f2e and cdb3450.

📒 Files selected for processing (2)
  • apps/web/app/api/veriff/webhook/handle-decision-event.ts
  • apps/web/app/api/veriff/webhook/route.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/app/api/veriff/webhook/route.ts

@socket-security
Copy link
Copy Markdown

socket-security bot commented Mar 27, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​veriff/​incontext-sdk@​2.5.09910010086100

View full report

Copy link
Copy Markdown
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: 3

🧹 Nitpick comments (1)
apps/web/lib/zod/schemas/partners.ts (1)

566-583: Redundant .pick() of identity verification fields.

EnrolledPartnerSchemaExtended already extends EnrolledPartnerSchema (line 566), which picks identityVerificationStatus and identityVerifiedAt at lines 479-480. The second .pick() at lines 579-580 is redundant.

Proposed fix
   .extend(
     PartnerSchema.pick({
       monthlyTraffic: true,
       industryInterests: true,
       preferredEarningStructures: true,
       salesChannels: true,
-      identityVerificationStatus: true,
-      identityVerifiedAt: true,
     }).shape,
   )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/zod/schemas/partners.ts` around lines 566 - 583,
EnrolledPartnerSchemaExtended redundantly re-picks identityVerificationStatus
and identityVerifiedAt via PartnerSchema.pick despite those being included by
EnrolledPartnerSchema; update EnrolledPartnerSchemaExtended to remove the
redundant identity fields from the PartnerSchema.pick call (or remove those two
keys from the .pick invocation) so PartnerSchema.pick only selects
monthlyTraffic, industryInterests, preferredEarningStructures, and
salesChannels; reference EnrolledPartnerSchemaExtended, EnrolledPartnerSchema,
PartnerSchema.pick, identityVerificationStatus, and identityVerifiedAt when
making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/web/app/`(ee)/partners.dub.co/(dashboard)/profile/identity-verification-section.tsx:
- Around line 97-104: Update the user-facing strings for the switch cases in
identity-verification-section.tsx so they end with a period for consistency:
modify the "expired" and "abandoned" branches that set failedReason (the
variable in the switch) to include a trailing "." at the end of each sentence.

In `@apps/web/app/api/veriff/webhook/handle-decision-event.ts`:
- Around line 113-117: The current logic only increments
toUpdate.identityVerificationAttemptCount when effectiveStatus is "approved" or
"declined", but terminal states "expired" and "abandoned" should also count as
consumed attempts; update the conditional in handle-decision-event.ts (the
branch that checks effectiveStatus) to include "expired" and "abandoned" so that
toUpdate.identityVerificationAttemptCount = { increment: 1 } runs for those
statuses as well, ensuring consistency with the
MAX_PARTNER_IDENTITY_VERIFICATION_ATTEMPTS enforcement in
start-identity-verification.ts.

In `@apps/web/app/api/veriff/webhook/route.ts`:
- Around line 58-64: Wrap the JSON.parse(rawBody) call in a try/catch to return
a controlled 400 response when parsing fails, then validate the parsed object
against veriffDecisionEventSchema and veriffSessionEventSchema before calling
the handlers: if ("verification" in body) call handleDecisionEvent only after
the body passes veriffDecisionEventSchema validation (otherwise return 400),
else call handleSessionEvent only after passing veriffSessionEventSchema
(otherwise return 400); ensure error messages are generic and do not leak
internal details.

---

Nitpick comments:
In `@apps/web/lib/zod/schemas/partners.ts`:
- Around line 566-583: EnrolledPartnerSchemaExtended redundantly re-picks
identityVerificationStatus and identityVerifiedAt via PartnerSchema.pick despite
those being included by EnrolledPartnerSchema; update
EnrolledPartnerSchemaExtended to remove the redundant identity fields from the
PartnerSchema.pick call (or remove those two keys from the .pick invocation) so
PartnerSchema.pick only selects monthlyTraffic, industryInterests,
preferredEarningStructures, and salesChannels; reference
EnrolledPartnerSchemaExtended, EnrolledPartnerSchema, PartnerSchema.pick,
identityVerificationStatus, and identityVerifiedAt when making the change.
🪄 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: 2ddcc15f-ab6d-413a-a3a5-43abb24d7083

📥 Commits

Reviewing files that changed from the base of the PR and between 72c7f2e and 57c636f.

📒 Files selected for processing (6)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/identity-verification-section.tsx
  • apps/web/app/api/veriff/webhook/handle-decision-event.ts
  • apps/web/app/api/veriff/webhook/route.ts
  • apps/web/lib/actions/partners/start-identity-verification.ts
  • apps/web/lib/zod/schemas/partners.ts
  • packages/prisma/schema/partner.prisma
✅ Files skipped from review due to trivial changes (1)
  • apps/web/lib/actions/partners/start-identity-verification.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/prisma/schema/partner.prisma

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