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
Vendoris a global identity, not aUser. TheVendormodel stores profile fields (name,email, etc.) with a unique email across the platform.- Per-org relationships live in
VendorOrgLink(vendorId+teamId), with statusINVITED | ACTIVE | REVOKEDand invite/magic-link fields scoped to that link. - Vendor endpoints use
VendorAuthGuardand@CurrentVendor(). Never createUserorUserTeamrows for a vendor. - The
ORG_USER_VENDORrole is removed and must not be reintroduced (enforced by thelint:no-vendor-roleCI 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
VendorOrgLinkfiltered byteamId, notUserTeam. - A vendor accepting an invite activates the link, not a user membership.
- Audit logs may record
actorVendorIdfor vendor-initiated actions. - Frontend services that call
/api/v1/vendorsreceiveVendorOrgLinkrows (with nestedvendor), notUsershapes — do not assume auserId. - 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