Skip to main content

ADR 003: Vendor identity is separate from User

Status: Accepted

Context

RiskFlow serves three actor classes: MSP, ORG, and VENDOR. Early designs treated vendors as a special user role (ORG_USER_VENDOR) or as rows in the same User / UserTeam tables as org staff. That conflated two different identity models:

  • Users belong to a team via UserTeam, authenticate with JWT cookies through Passport, and are authorized with RBAC from @riskflow/permissions-utils.
  • Vendors are external parties (BAAs, training assignees, agreement signers) who may work with multiple orgs over time using the same email address.

Mapping vendors onto User caused duplicate identities, incorrect RBAC checks, and leaks when vendor endpoints reused @CurrentUser() instead of a vendor principal.

Decision

  1. Vendor is a global identity, not a User. The Vendor model stores profile fields (name, email, etc.) with a unique email across the platform.
  2. Per-org relationships live in VendorOrgLink (vendorId + teamId), with status INVITED | ACTIVE | REVOKED and invite/magic-link fields scoped to that link.
  3. Vendor endpoints use VendorAuthGuard and @CurrentVendor(). Never create User or UserTeam rows for a vendor.
  4. The ORG_USER_VENDOR role is removed and must not be reintroduced (enforced by the lint:no-vendor-role CI script).
Vendor (global) VendorOrgLink (per org) Team (ORG)
───────────── ─────────────────────── ──────────
id, email (unique) ←── vendorId, teamId, status ──→ id, type=ORG
name, phone... inviteToken, magicLink...

Consequences

  • List vendors for an org by querying VendorOrgLink filtered by teamId, not UserTeam.
  • A vendor accepting an invite activates the link, not a user membership.
  • Audit logs may record actorVendorId for vendor-initiated actions.
  • Frontend services that call /api/v1/vendors receive VendorOrgLink rows (with nested vendor), not User shapes — do not assume a userId.
  • Cross-org vendor access is validated through the link for the target org's teamId, not through a global vendor role.

References

  • Schema: apps/backend/prisma/schema/documents.prisma (Vendor, VendorOrgLink)
  • Migration: 20260526120000_vendor_identity_redesign
  • CLAUDE.md — Vendor Identity (Critical)
  • apps/backend/src/modules/vendors/ — vendor HTTP layer