Overview
The codebase tracks project membership and manager status in four separate places simultaneously. ProjectTeamMember was designed as the canonical join table for this relationship, but the app never fully committed to it — resulting in redundant, potentially out-of-sync fields on both User and Project that are used in its place.
Action Items
Research and document the current inconsistency
The four representations of the same two facts ("is a member" / "is a manager"):
| Field |
Model |
Tracks |
User.projects[] |
User |
basic project membership |
User.managedProjects[] |
User |
manager status (denormalized on user) |
Project.managedByUsers[] |
Project |
manager status (denormalized on project) |
ProjectTeamMember.vrmsProjectAdmin |
ProjectTeamMember |
manager status (join table, currently unused) |
The updateManagedProjects controller keeps User.managedProjects and Project.managedByUsers in sync with each other, but it never touches ProjectTeamMember.vrmsProjectAdmin. Any code reading vrmsProjectAdmin may see stale data.
Evaluate consolidating onto ProjectTeamMember as the single source of truth
ProjectTeamMember already has everything needed:
ProjectTeamMember {
userId,
projectId,
teamMemberStatus, // Active / Inactive
vrmsProjectAdmin, // Boolean — manager flag (already exists, currently bypassed)
roleOnProject, // Developer, PM, UX, etc.
}
If the app read/wrote through this model exclusively:
- "Is user a member?" →
ProjectTeamMember.findOne({ userId, projectId })
- "Is user a manager?" →
ProjectTeamMember.findOne({ userId, projectId, vrmsProjectAdmin: true })
- "All of a user's projects with manager status?" →
ProjectTeamMember.find({ userId }) — one query returns both facts per row
This would allow removing User.managedProjects, Project.managedByUsers, and User.projects as redundant fields.
If consolidation is approved, the migration would involve:
- Backfill — for every entry in
User.managedProjects, find or create the corresponding ProjectTeamMember record and set vrmsProjectAdmin: true
- Update all write paths — the
updateManagedProjects controller writes to ProjectTeamMember.vrmsProjectAdmin instead of the denormalized fields
- Update all read paths — anything reading
User.managedProjects or Project.managedByUsers queries ProjectTeamMember instead
- Drop the redundant fields once all paths are migrated
Note: Issue #2149 (manager toggle feature) requires reading from ProjectTeamMember to know a user's non-manager memberships. That ticket should write to vrmsProjectAdmin to move toward the clean model rather than adding another consumer of the redundant fields.
Resources/Instructions
Overview
The codebase tracks project membership and manager status in four separate places simultaneously.
ProjectTeamMemberwas designed as the canonical join table for this relationship, but the app never fully committed to it — resulting in redundant, potentially out-of-sync fields on bothUserandProjectthat are used in its place.Action Items
Research and document the current inconsistency
The four representations of the same two facts ("is a member" / "is a manager"):
User.projects[]User.managedProjects[]Project.managedByUsers[]ProjectTeamMember.vrmsProjectAdminThe
updateManagedProjectscontroller keepsUser.managedProjectsandProject.managedByUsersin sync with each other, but it never touchesProjectTeamMember.vrmsProjectAdmin. Any code readingvrmsProjectAdminmay see stale data.Evaluate consolidating onto
ProjectTeamMemberas the single source of truthProjectTeamMemberalready has everything needed:If the app read/wrote through this model exclusively:
ProjectTeamMember.findOne({ userId, projectId })ProjectTeamMember.findOne({ userId, projectId, vrmsProjectAdmin: true })ProjectTeamMember.find({ userId })— one query returns both facts per rowThis would allow removing
User.managedProjects,Project.managedByUsers, andUser.projectsas redundant fields.If consolidation is approved, the migration would involve:
User.managedProjects, find or create the correspondingProjectTeamMemberrecord and setvrmsProjectAdmin: trueupdateManagedProjectscontroller writes toProjectTeamMember.vrmsProjectAdmininstead of the denormalized fieldsUser.managedProjectsorProject.managedByUsersqueriesProjectTeamMemberinsteadNote: Issue #2149 (manager toggle feature) requires reading from
ProjectTeamMemberto know a user's non-manager memberships. That ticket should write tovrmsProjectAdminto move toward the clean model rather than adding another consumer of the redundant fields.Resources/Instructions
backend/models/projectTeamMember.model.js— join table schemabackend/models/user.model.js—projectsandmanagedProjectsfieldsbackend/models/project.model.js—managedByUsersfieldbackend/controllers/user.controller.js—updateManagedProjects(the current write path)