Building "Login As Mom" Without Building a Permissions System
How MyVitals lets one family member act as another with full read/write access, why the permission check is never cached, and the req.userId vs req.effectiveUserId footgun.

Every app with shared access eventually gets asked for "can my daughter manage my account for me." My instinct was full RBAC. What MyVitals actually needed — one person managing a household's lab reports — was closer to "let me act as this person entirely," not granular per-resource permissions. Building it safely was the interesting part.
The model: an invite is a membership, not a separate thing
FamilyMember is one row per (familyId, invitedEmail), representing a pending invite and an accepted membership as the same document transitioning state. userId stays null until the invited email matches an account.
One detail worth stealing: the invite token is cleared on acceptance via undefined, not null. There's a sparse unique index on it, and sparse indexes only exclude absent fields — null still counts as a value and collides the moment a second person accepts. One character between "works" and "second invite ever throws a duplicate key error."
The permission check is one function, and it is never cached
async function canActAs(trueUserId, targetUserId) {
if (trueUserId === targetUserId) return true;
return sharesAcceptedFamilyWith(trueUserId, targetUserId); // live DB read
}
Called on every request, not cached, not memoized. This is the decision I'd defend hardest: cache it and "removing someone revokes access immediately" becomes "...within roughly five minutes." One extra DB read per request buys a security property that's actually true instead of one that's true most of the time.
Query param, not header
?asUserId= — because some frontend call sites are plain <a href>/<img src> tags with no way to attach a custom header. A query param works everywhere a URL works.
Two identities, one footgun
req.effectiveUserId— whose data to read/write. Query by this.req.actingUserId— who's really making the request, for audit fields.
Using req.userId directly in a new controller doesn't error — it silently breaks acting-as. A family member switches to their mother's profile, uploads a report, and it lands under the acting user's own account instead. No warning, just a report filed under the wrong person's medical history. There's no type system catching this (plain ESM JS) — it survives on convention and code review alone.
What this deliberately doesn't do
No granular permissions, no "view but not edit." Symmetric, all-or-nothing, targeting one person managing a household — not a care team with clearance levels. If that ever changes, this model gets replaced, not extended.
👉 Try MyVitals now — invite a family member and switch profiles in ten seconds.


