Last updated on February 21, 2021.
One of the most valuable features Bullet Train provides for Rails developers is the implementation of collaborative teams, including invitations and user roles.
The reason this feature is so valuable is that in so many situations developers and even product teams won’t consider “Teams” to be an MVP feature when developing a new product. That’s fair. I also wouldn’t have wanted to delay the launch of a good number of applications I’ve built to wait for the design, implementation, and testing of a feature as complicated as teams.
However, those of us who have skipped implementing teams in an application early on and then found ourselves having to implement it later can attest to how challenging it can be, because so much of your domain model, controllers, and permissions were built around resources belonging to an individual
User account. It’s not just a feature, it becomes a major refactor.
With that introduction in mind, I'm going to walk through both the principles and implementation details that have made the Bullet Train approach to multitenancy so successful across so many different types of applications. This is primarily intended to be a guide to help incoming Bullet Train developers understand the system they're inheriting, but I'm also hopeful this could be helpful to folks in the broader software community who are trying to figure out how to model things in their own multitenant systems.
So then, jumping right in!
Most of your domain model should probably belong to a team, not a user.
The resources in your domain model will typically end up belonging to one of three things:
- the entire system
- a joint organization
- an individual user
The components that belong to the entire system are those resources that span across multiple tenants, such as global configuration or resources, data and logic your application is specifically designed to provide to your users. To keep things simple for now, let’s set those aside in our considerations here.
Between the other two (resources that belong to either joint organizations or individual users) my experience is that most software applications should model most of the primary components of their domain model as belonging to the joint team or organization, even in a lot of situations where a software developer will first come to the conclusion that it should belong to individual users.
For example, if you’re implementing a two-sided marketplace that helps an upcoming bride or groom connect with a photographer to capture the memories on their special day, many developers (including my 2012 self, when I first built the platform that now powers Zola’s vendor directory and real wedding inspiration) might make modeling decisions like:
The Yelp-esque business listing belongs to a photographer.
A bride/groom can submit an inquiry to a photographer or business listing.
When an inquiry is submitted, we save the wedding date provided on the bride/groom’s account so we can pre-populate it for inquiries to other photographers.
However, when it comes to domain modeling, the language we use to describe things matters a lot. When it comes time to implement, there is a big difference between the statements I made above and what those statements actually should have been:
The Yelp-esque business listing belongs to a photography company. A photography company can have many photographers and other employees working in support roles such as marketing and accounting, all of whom may have a vested interest in the business listing.
Couples can submit an inquiry to a photography business. Anyone at the business can respond to the inquiry and both members of a couple can participate in the resulting conversation.
When an inquiry is submitted, we save the wedding date provided to the couple’s profile so we can pre-populate it for inquiries to other photography businesses.
And also, lest we be tempted to conceptualize the business listing as “the team”:
A photography business can have multiple Yelp-esque business listings in different markets.
Even in software that isn’t primarily collaborative, there are so many examples where I talk to developers and they describe their domain model in terms of resources belonging to users when they really should belong to some sort of joint team so that the initial user at least has the option of inviting other people to help them maintain whatever resources are in the system without having to pass credentials around.
If all else fails, ask yourself this:
If you’re still wondering whether that resource you’re going to add to your domain model should belong to a team or an individual user, just ask yourself: “What if a user wants their assistant to help them with this?”
Again, even if the team that a resource belongs to consists of a single user 98% of the time, it’s still likely that your most sophisticated users will want to be able to share access to that account with someone, e.g. celebrities or marketing departments on Twitter.
Even in a situation where the owner of a resource is, for example, a student and it feels obvious that the student is always going to be an individual
User, consider that it might be valuable to allow them to invite and onboard another
User into to their account for situations where they require guardian consent.
Many thanks to @andrewculver for taking the time to walk me through why teams matter for a 2-sided marketplace. I didn't fully grasp the importance until I began adding capabilities to tutors and students that were initially out of scope.— Don Pottinger (@donpottinger) February 21, 2021
In a two-sided marketplace or system, a
User should be able to use the same account to participate in both sides of the marketplace or system.
I didn’t mention this in the example above, but another mistake I made in the early days at the wedding marketplace startup was implementing
User accounts something like so:
Usercan represent a wedding photographer or a bride/groom.
It should have been:
Usercan represent a wedding photographer that shoots second camera for multiple wedding photography companies and can also get engaged and then plan their own wedding with their fiancée on our platform.
You know what would have made that a lot easier? If we had modeled everything as teams (along with the “team types” I’ll talk about later) from the beginning.
So what do you call these teams?
Until now I’ve been making reference to the “joint team” or “organization”, but depending on the domain of your application you might also refer to it as the company or family or school or community or any other number of words used to describe groups of people.
In Bullet Train it’s called a
Team, but developers can refer to it as whatever they want in the UI by editing
config/locales/en/teams.en.yml where all the references to “Team” or “Teams” are localized.
Of all the examples mentioned above, I think the worst label you can give a team is “account”. I think people are sometimes tempted to call it that because in most applications the responsibility for billing rests with the team. However, the problem in practice with trying to call the team an “account” is that the vast majority of people associate the word “account” with user accounts, so trying to use the same word in the context of a “team account” is just plain confusing.
In retrospect, I do sometimes wish I had called the actual class
Organization instead of
Team, because the two are basically equivalent in the way people understand them, but between the two,
Team is more likely to conflict with concepts in the domains that software is being written for. I more than almost anyone should have picked up on that, since one of the first Rails applications I ever wrote was for managing youth sports leagues. However, I didn’t and I think it will probably be
Team in Bullet Train for quite a long time.
What about invitations, memberships, roles, etc.?
In Bullet Train, the rest of the domain model looks like this:
Userbelongs to a
Membershipcan have one or more
Roles associated with it.
Membershipmodel itself can be extended by developers to include additional types of permissions that can be configured on a per-
Membershipbasis, e.g. access to certain features or specific resources.
- When adding a new
Invitationis also created and sent via email.
Invitationcan be claimed by either a new or existing
User, even if the email address doesn’t match where the invitation was delivered too.
Invitationgoes away once claimed.
Also, when billing is enabled and relevant:
Subscriptionbelongs to the
Team, not a
- When billing is “per user”, the
quantityof subscriptions (in Stripe’s language) can simply be implemented as
Team. (You can get more creative with this logic if, for example, you want to only charge for active users.)
Resource assignments and references should be to
Membership and not
This doesn’t seem to come naturally to developers at all, but it’s important because it unlocks a much improved user experience for the folks who are responsible for both inviting new team members into a system.
Consider an example of our actual implementation of at-mentions within a
If at-mentions were associated directly with a
User, a team administrator would need to wait until an invited team member actually created an account and claimed their
Invitationbefore there is a
Userto at-mention. They can’t start including that incoming team member right away. This is similar to systems like GitHub, where you can’t assign issues to a user until they’ve accepted their invitation. It’s unideal.
If at-mentions are associated with a
Membership, then a team administrator can start including the incoming team member in discussions as soon as they’ve had the idea to add them. The messages will be waiting in that team member’s inbox if and when they accept the invitation to join the team, even if they didn’t have a
Useraccount previously. This is ideal.
How can this be improved?
There used to be an entire section in this post, under this headline, highlighting the improvements we were planning to make to the multitenancy domain modeling in Bullet Train. However, we completed the massive refactor that was required to accomplish all of what was discussed in the previous section in early 2020. At this point, I think the largest outstanding improvement is:
- I don’t think
Invitationactually needs to exist as its own model anymore, I think it can probably be rolled into
- I'm also pretty sure
Rolecan go away as a model as well, and instead these assignments can be persisted as an array of keys on the
How can this be extended?
The system described above has been able to either accommodate or be extended to accommodate every type of collaborative application that has been built on Bullet Train. It doesn’t handle every single situation out-of-the-box, but it’s always been a start in the right direction.
Some Bullet Train applications have been extended with even more advanced team functionality:
Team can also be associated with a specific “team type” so as to differentiate (for example) households from businesses, schools from families, etc.
Parent and Child Teams
Sometimes different team types end up falling into a parent and child relationship. For example, a school using your multitenant software might need to invite a family to sign up in order to interact with them, but the family is signing up to engage specifically with the school and the school technically owns the family’s
Team. We provide an example of how to implement this as a feature branch in Bullet Train.
Team can be invited to collaborate as part of another
Team. This is especially useful in applications where a business might want to pull in an entire agency to collaborate on a project.
We’d like to open source this!
It’s been my intention for awhile to open source this foundational piece of Bullet Train so that all Rails developers can be done with this once-and-for-all. I don’t think my solution is perfect, but it’s worked really well for a lot of people, has been extended into some pretty complex configurations, and has been deployed in some pretty sophisticated places, and I think it can only get better with contributions from more people and exposure to other applications that won’t ever be built on Bullet Train.
So many of the collaborative features that have been added to Bullet Train in the past 3+ years were only possible to build and ship for customers because of the many assumptions I could make about the way teams were implemented and permissions were set up. If something like this becomes broadly adopted in the Rails ecosystem, I think it could unlock a bit of a renaissance of library development for higher-level features than what a lot of libraries are able to provide now. I want to continue pushing forward the idea that Rails is still the best tool to build and deploy new web applications quickly.
There are existing Bullet Train customers who have expressed an interesting in participating in this open-source endeavor. If you’re interested in participating, feel free to reach out to me via Twitter DM directly. If you’re not on Twitter, email works as well. (It’s my first name at the root domain of this page.) Any projects will adopt the Ruby on Rails Code of Conduct and the Contributor Covenant.