Why MyMusicStaff parents keep getting locked out of their portal — and what the BCC pattern reveals
The parent-portal complaints in MMS reviews trace back to one design assumption: one login per family. A close reading + the per-contact token model that doesn't break.
Reading the Capterra reviews of MyMusicStaff for a long enough stretch produces a specific pattern I want to name: the same complaint, in different words, from different teachers, all pointing at the same architectural seam. It's not lesson notes (that's a different post) and it's not billing reports. It's the parent portal — specifically, two-parent and split-household families — and the email-thread side effect that gets written up as the "BCC pattern."
The verbatim line that cleanest describes the email side of it, from the public Capterra review corpus:
"Emails cannot be sent to students and parents simultaneously — they're automatically BCC'ed, leading to confusing message threads and miscommunications."
— Capterra review of MyMusicStaff, surfaced via the public review feed
And the family-side complaint, from a different source — Joseph at Flex Lessons, who runs a multi-platform comparison — phrased the cousin issue at Fons (a different vendor with the same architectural fault) this way:
"the lack of full support for account family members"
Two products. Two reviewers. Same root cause. I want to take it apart.
The assumption that makes the portal fragile
The shared design choice across MyMusicStaff, Fons, and most of the legacy studio-management category is this: a family is one identity in the system, and the parent portal logs in as that identity.
There are reasonable historical reasons for this. The first generation of studio-management tools was built when "the family" was the billing entity, and the parent portal was modeled as a window onto that family's billing record. One household, one invoice stream, one login.
The reason it's fragile in 2026 is that families don't actually work that way, and they didn't really work that way in 2010 either — but the architecture has now been ratified by enough downstream code that it's expensive to change.
Three concrete failure modes flow directly from this assumption.
Two parents in the same household. Mom handles the billing, Dad handles the schedule. Both want their own login. The "one identity per family" model gives them shared credentials — which means a password reset by Mom logs Dad out, every reset notification cc's both parties, and the audit log shows "the family" did things rather than "Mom did the thing on Tuesday." When the password gets shared via text and Dad's phone is the one that auto-fills it, the reset cycle becomes: Dad clicks the reset link in Mom's email (because they share an inbox), the reset succeeds in Dad's session, Mom is now logged out of her own portal. Repeat.
Split households. Mom has primary custody, Dad has alternating weekends. Both pay a portion of lessons, both want to see the schedule, neither wants to coordinate the password. In the one-login model, somebody loses. The losing parent typically calls the studio for status updates, which converts a portal-saving feature into a phone-call-generating one.
Two students with two different sets of guardians. Older sibling lives with Dad, younger sibling lives with Mom-and-stepdad. They're enrolled in the same studio. The "family" abstraction can't represent this without compromising; the studio either creates two separate "families" (and now lessons can't be shared, billing is duplicated, and the portal is two distinct logins for what's still functionally one studio relationship) or one family (and one of the parents gets visibility into the other household's billing).
The "BCC pattern" is the same architectural seam viewed from a different angle. Because the family-identity has multiple email addresses attached, any email the studio sends to "the parent" has to fan out to all of them. The system implements this as a BCC — to all parent emails on the family record, plus the student's email if she has one. The BCC has the predictable consequence: replies aren't threaded, half the parents reply to the studio without the other half seeing it, and the conversation about Tuesday's reschedule splits into three private threads that nobody can reconcile a week later.
The lockout reviews and the BCC reviews are not two complaints. They're one architectural decision producing two different field complaints.
What "per-contact" means in Segnoly
Segnoly inverts the assumption. The smallest identity in the system is a contact, not a family. Contacts are linked to students via a many-to-many table (db/schema.ts:studentContacts), so a single student can have multiple contacts and a single contact can be attached to multiple students. There is no "family" entity at all. There is only a graph of contacts and students with edges between them.
The parent portal is built on top of this graph. The relevant code lives in three files:
lib/portal.ts
generatePortalToken()
hashPortalToken(token)
signPortalSession(contactId, expiresAtMs)
verifyPortalSession(cookie)
app/api/contacts/[id]/portal/route.ts — issue a token for one contact
app/api/contacts/[id]/portal/send/route.ts — mail the magic link to that contact
app/portal/[token]/... — the portal pages themselves
The structure that matters: the token is bound to a contact id, not a family. When the studio invites Mom to the portal, app/api/contacts/[id]/portal/send/route.ts issues a token that says, in effect, "this magic link logs in as Mom." When the studio invites Dad, the same endpoint runs with a different id and issues a different token, bound to Dad. The two tokens are independent. They expire independently. They get revoked independently. Mom and Dad never share a password, because there is no password — the link is the credential, and the link belongs to one contact.
For a family of five with two parents in different households and two students enrolled, that means up to five tokens (parent-A, parent-B, parent-C if there's a stepparent, student-A if she's old enough, student-B if he's old enough). Each one is its own login. None of them step on each other's sessions. None of them have to be coordinated by the studio. The reset flow is one-contact-at-a-time: parent-B forgets her token, the studio re-issues a new one for parent-B's contact id only, and parent-A's session is untouched.
The signed cookie that's set after the magic link is consumed (signPortalSession in lib/portal.ts) carries only the contact id. There is no family scope, because there is no family table to scope to. Authorization for "what can this session see?" is computed at request-time by walking the contact's edges in studentContacts — which students is this contact attached to? — and returning only those students' data. Mom's session sees student-A and student-B because she's attached to both. Stepdad's session sees only student-B because he's attached only to that one. The session can never accidentally surface a student that the contact isn't attached to, because the data fetch is rooted on the contact's edges.
What this changes about email threading
The BCC problem disappears when the email model is per-contact too. In Segnoly, the studio's "email this parent" action sends a single email to a single contact's address. There is no fan-out. If the studio wants both parents to see a message, the action is "send to both parents" and the result is two separate emails, one per recipient — same content, but each addressed individually, threading independently in each parent's inbox, replied to independently.
The architectural property that makes this clean is that the contact, not the family, is the unit of communication. When parent-A replies to her copy, the reply lands as a reply to parent-A's thread. When parent-B replies to his copy, the reply lands as a reply to parent-B's thread. The studio sees both, properly attributed in the message log, and the conversation about Tuesday's reschedule has two clean transcripts instead of one BCC mess.
This isn't a feature. It's the absence of a workaround. The BCC pattern is what you have to do when your data model conflates multiple identities into one; when the data model represents them as distinct, the BCC pattern stops being necessary, and the email threading just works the way email threading was designed to work.
Why the legacy products can't fix this without a rewrite
A reasonable question, if you've been at this for a while, is: why doesn't MyMusicStaff just fix it? The answer is: the family-as-identity assumption is in the foundations, and the foundations have been load-bearing for a decade.
If the families table is referenced by the invoices table, the lessons table, the messages table, the attendance table, and the portal_logins table — and if every one of those relationships is one-to-many on the family side — then "split this family into per-contact identities" isn't a feature you ship in a sprint. It's a multi-month migration that touches most of the schema, every billing path, every email path, every reporting path, and every customer's existing data. The risk-reward is bad. So instead, you ship "let parents add a second email address to the family record," and the BCC pattern persists, and the six other reviewers I read while writing this keep typing variations of the same complaint into Capterra.
Segnoly didn't have to make that tradeoff because Segnoly was designed in 2025 with the per-contact model from day one, after I'd read enough reviews to know exactly which assumption to invert. The cost of inverting it on a greenfield codebase is approximately zero — a different schema, a different cookie, a different fetch root. The cost of inverting it on a ten-year-old codebase is everything.
What this means if you're shopping
The fifteen-second test that surfaces the underlying identity model: ask the vendor whether two parents in the same family can have two independent logins, with two independent passwords (or two independent magic-link tokens), with two independent reset flows. If the answer is "they share credentials" or "we recommend each parent uses a different email so the family record can route to one," the product is built on the family-as-identity assumption and the lockout pattern is a permanent feature.
The follow-up test: ask whether a single email to "both parents" is one email BCC'd to two addresses, or two separate emails. Same architectural question, different surface. The answer tells you whether the conversations with parents are going to thread cleanly or split three ways.
Segnoly passes both tests because the identity in the system is the contact, not the family. The token model in lib/portal.ts is roughly thirty lines of code, the contacts-to-students graph is one join table, and the result is a parent portal that doesn't lock anyone out and an email log that doesn't BCC.
That's the entire architectural difference. The lockout reviews and the BCC reviews are how it shows up in the field.
Building Segnoly — billing and automation for independent music teachers. The token code in this post is real and lives at lib/portal.ts, and the waitlist is open for the first cohort.