ProjectTeamMember schema is the right join table but is bypassed by redundant fields on User and Project
- Dominant language
- JavaScript
- Stars
- 61
- Forks
- 101
- PR merge metrics
- No merged PRs in 30d
Description
### 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:**
1. Backfill — for every entry in `User.managedProjects`, find or create the corresponding `ProjectTeamMember` record and set `vrmsProjectAdmin: true`
2. Update all write paths — the `updateManagedProjects` controller writes to `ProjectTeamMember.vrmsProjectAdmin` instead of the denormalized fields
3. Update all read paths — anything reading `User.managedProjects` or `Project.managedByUsers` queries `ProjectTeamMember` instead
4. 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
- `backend/models/projectTeamMember.model.js` — join table schema
- `backend/models/user.model.js` — `projects` and `managedProjects` fields
- `backend/models/project.model.js` — `managedByUsers` field
- `backend/controllers/user.controller.js` — `updateManagedProjects` (the current write path)
- Related: #2149 (manager toggle feature that surfaces this inconsistency)
Contributor guide
Assessment
This issue has not been assessed yet.