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.
Almost everything your domain model should belong to the team.
The resources in your domain model will typically end up belonging to one of three things: The entire system, a joint organization, or 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. Let’s set all of 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.
In a two-sided marketplace, a
User should be able to use the same account to participate in both sides of the marketplace.
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
Invitationis sent via email and can be claimed by either a new or existing
User, even if the email address doesn’t match where the invitation was delivered too.
- Both an
Membershipcan have one or more
Roles associated with it. When an invitation is claimed, whatever
Roles were associated with it are transferred to the resulting
- Developers can extend both
Membershipto include associations to other resources that a
Usercan specifically be granted access to, or developers can add new attributes that represent whether a
Userhas special permissions, like being granted access to all resources of a specific type.
- Where billing is present, the subscriptions belongs to the
- When billing is managed “per user”, the quantity of 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.)
How can this be improved?
When I first published this article yesterday, I said the following under the heading "What are the current short comings?":
At present, the biggest shortcoming in the current implementation is that there isn’t a streamlined solution for assigning resources to someone who hasn’t claimed their invitation yet. For example, in Bullet Train’s built in at-mention support, the list of team mates is populated from
team.users. Likewise, most “assign to” field are typically implemented by developers as either a
has_many … through:with either a
Membershipon the other end. However, from a UX perspective in the Bullet Train applications I maintain, I know there needs to be a better default where you can reference or assign to team members that haven’t accepted their invitation yet, and have those notifications delivered to the appropriate email address and have those resources and conversations properly assigned when the
Well, no sooner did I publish the article than my friend Joe DelCioppio responded with a suggestion that unlocked a whole bunch of things for me conceptually. I'll update this article properly at some point in the future, but for the time being I've captured my thoughts in this tweet:
@thedelchop Going to post this here for anyone following along at home, but this is what your comments inspired for me. It's probably not exactly the same as what you're describing, but it's a solid step forward from what I have today and feels really good at first blush. pic.twitter.com/j2OomFB6CR— Andrew Culver (@andrewculver) February 2, 2020
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 situation out-of-the-box, but it’s always been a start in the right direction.
Although not provided by default in Bullet Train, some Bullet Train applications have been modeled with even more advanced team functionality, and it’s likely these will become available for general use as well:
Team can also be associated with a specific “team type” (modeled as
Teams::Type) so as to differentiate “couples” from “businesses”, “schools” from “families”, etc.
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 actually open source this foundational piece of Bullet Train so that all of us 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 2+ 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.