Skip to content
heapbyte - A name of excellence

Architecture · 2 October 2026

A role belongs to the membership, not the person

Anna leads the under-twelves and plays for the under-fourteens. The system knows both things: her customer record carries role:leader, team:u12 and team:u14, which is accurate in every particular. On Monday she opens the leader report to see how her team’s fundraising went, and it shows her the under-twelves’ sales — and the under-fourteens’. Her own teammates, by name, with totals. Nobody configured that. Every tag is correct, and together they say something nobody meant.

7 min read
Written by the HeapByte engineering team

Shopify tags handling

Tags describe a person

A customer tag is a label on one record. A customer can carry up to 250 of them, each up to 255 characters, and they are very good at what they are for: filtering, segmenting, a quick “show me everyone in this club”. The payout ledger article made the point in passing that a hierarchy does not fit in them, and this one is about the specific way it does not fit, because that is where the damage happens.

The problem in the opening is not that tags are messy. It is that every fact about Anna was written onto Anna, and one of those facts was never about her alone.

The role is on the edge

“Leader” is not a property of a person. It is a property of a person’s relationship with one team. Anna is a leader of the under-twelves. She is a member of the under-fourteens. Those are two relationships, each with its own role, and the moment the role is stored on the person rather than on the relationship, the two collapse into one: a leader, who is in two teams.

So the model is one row per person per team, and the role belongs to the row. That is the whole idea, and it is not novel — it is how any membership system that has survived contact with real organisations ends up. What makes it worth writing about is how naturally the other version arrives in a Shopify build, where the customer record is right there and tags are free.

The same collapse happens to credit, not just to sight, and it is quieter. A fundraising sale is attributed to a seller, and the totals roll up — seller to team, team to club. If Anna sells for both her teams and the link she shares identifies only Anna, every sale she makes is ambiguous at the first roll-up: it belongs to Anna, and Anna belongs to two teams. Somebody writes a rule to break the tie, usually “the first team on her record”, and one team’s total is quietly short for the rest of the season. Nobody notices, because the club total is right.

The fix is the same shape as before. The thing a sale is attributed to is the membership — Anna in the under-fourteens — not the person, so the link carries both and the roll-up has nothing to guess. Attribution and visibility are the same question asked from opposite ends: which relationship does this fact belong to.

Shopify already has this shape, for buyers

Shopify’s own model gets this exactly right, in a different context. In B2B, a customer is attached to company locations, and permissions are assigned per location: Ordering only, or Location admin, which can see every order placed for that location. A person can be an admin at one location and an orderer at another. The role is on the edge.

If your hierarchy is a set of buyers purchasing on behalf of an organisation, use it. It is native, it is supported, and it is already the correct model.

It does not fit sellers, for two reasons. A customer can belong to only one company, and the people in the opening belong to more than one structure by design — the case study names exactly this, leaders and members participating across multiple organisational structures. And B2B visibility is over orders placed for a location. A fundraising leader needs to see sales attributed to the people they lead, which are orders placed by members of the public who have never heard of the team.

Holding the structure is not the hard part

Shopify can also hold the structure itself. A metaobject for each team, with a customer reference for the leader and a list of customer references for the members, is expressible natively and editable in the admin. If the requirement is to record who is in which team, that is enough.

What it does not do is answer who may see what. A customer account shows a customer their own orders. There is no notion in Shopify of a person who is not staff and may see other people’s sales because of where they sit in an organisation — and that question, not the membership list, is what the hierarchy is for.

On the Säsongsgruppen platform, the structure — clubs, teams, leaders, members and the products assigned to them — lives in the application’s own database, and Shopify holds the orders and the seller attribution that points back into it. Visibility is derived from the structure, every time it is asked.

ts
type Role = "member" | "leader" | "club-admin";

/** One row per person per team. The role belongs to the row, not the person. */
type Membership = { personId: string; teamId: string; clubId: string; role: Role };

/**
 * Which sellers' sales may this person see?
 *
 * Everyone sees their own. A leader sees the members of the teams they lead,
 * and only those. A club admin sees every seller in that club.
 */
export function visibleSellers(viewerId: string, memberships: readonly Membership[]): Set<string> {
  const visible = new Set<string>([viewerId]);
  const own = memberships.filter((m) => m.personId === viewerId);

  const ledTeams = new Set(own.filter((m) => m.role === "leader").map((m) => m.teamId));
  const adminClubs = new Set(own.filter((m) => m.role === "club-admin").map((m) => m.clubId));

  for (const m of memberships) {
    if (ledTeams.has(m.teamId) || adminClubs.has(m.clubId)) visible.add(m.personId);
  }
  return visible;
}
The line that matters is the filter on role === “leader” before collecting team ids. The version that tags produce asks a different question — is this person a leader, and which teams are they in — and for anyone who leads one team and plays in another, the two answers combine into authority over a team they only belong to. Nothing errors. The leader’s report simply includes their teammates. Holding the role on the membership row makes that combination impossible to express, which is a stronger guarantee than remembering not to write it.

Ask the rows, every time

The function is short because the model does the work. Every question about visibility becomes a question about rows, and a row can only say one thing about one relationship.

What this does not do

It is current state only. Memberships change between seasons, and a leader’s authority starting in March is a fact the report needs if it is going to show last autumn’s figures correctly. The real rows carry dates; the function above does not, because the point it makes is about where the role lives, not when.

It is not authentication. It answers what an identified person may see. Who that person is, and how they proved it, is a separate problem with its own failure modes, and neither customer tags nor membership rows solve it.

And it does not decide policy. Whether a club admin should see individual sellers or only team totals is the organisation’s call, and it changes. The model makes either rule easy to write and neither rule automatic.

When this needs an engineer

Often it does not. If tags are labels for filtering — a club tag for an email segment, a team tag for a report you run yourself — they are the right tool, and replacing them with a database is cost without benefit. If your organisation buys rather than sells, Shopify B2B already has the model, and you should use it.

It needs engineering when people who are not staff must see data about other people, and what they may see depends on where they sit in a structure that changes. That is the point where a tag stops being a label and starts being a permission, and a permission written as a string on a customer record is one nobody can audit. That work sits with our other custom Shopify applications, and it is what custom Shopify app development is for when the structure has outgrown the record it was written on.

Send us the store and the symptom.

---

Insights

Apply this to your store.

An audit turns the general principle into a specific list of changes, ordered by what actually pays back.