MyVitals
Engineering

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.

S

Suyash Vashishtha

3 September 2026 · 2 min read

ShareWLF
Building "Login As Mom" Without Building a Permissions System

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.

Track your own lab reports

Upload a report from any lab, in any format. MyVitals reads every value and builds one continuous history you can actually follow.

General health information, not medical advice. Always consult a qualified healthcare professional.

Related reading