At MilkStraw AI, we use Ruby on Rails for our web application and manage user authentication with the well-known and battle-tested Devise gem. (Yes, it’s as simple as that! 😁) Our relational model is straightforward: each user can create multiple accounts, and each account belongs to one and only one user. All other relations, such as billing and account-related data, must be associated with one of these two models.

The Challenge
When we first discussed adding a teams feature, I was convinced that Devise would have a plugin for it—something like:
But I was wrong.
Since there was no out-of-the-box solution, I began gathering requirements from the business team. There’s no universal rule of thumb for implementing teams, so after multiple rounds of discussions, we agreed on these key requirements:
Users can create and belong to multiple teams.
A team can have multiple users.
Users should be able to leave a team.
Users can have one of five roles in a team:
Viewer: Can view accounts in the team and their recommendations.
Finance: Can review bills and the team's financial status.
DevOps: Can approve recommendations in addition to the Viewer abilities.
Owner: Can perform all the above actions.
Primary Owner: Similar to an Owner but can also rename or delete the team.
Owners can invite and remove users from the team.
Billing should be handled at the team level.
Build vs. Buy
After defining our requirements, we debated whether to use a third-party solution like Auth0 or build the feature ourselves. We evaluated third-party providers and weighed their benefits against the complexity of migrating our current system to their services and the risk of facing some limitations with their systems in future. Ultimately, we decided to implement the teams feature in-house and not rely on third-party solutions for now.
Implementation
I started by modifying our relational model to introduce a new Team model that manages user and account associations.
A Team can have multiple accounts, but each account belongs to only one team.
A Team can have multiple users, and each user can belong to multiple teams.

Using this updated relational model, I wrote a database migration to create a team for each user and move their existing accounts into the appropriate teams. Since we use the Pay gem for billing, I also wrote another migration to relink Pay records with teams instead of users.
I chose database migrations over Rake tasks for this step because our data size was manageable, and the migration wouldn't impact application uptime.
This initial database change was the easy part—then came the real challenges.
Frontend Challenges
The first major hurdle was designing the teams dropdown for a seamless user experience. After exploring different approaches and holding several discussions, we took inspiration from Vercel’s teams and projects dropdown.
We use Phlex for templating, RubyUI as our component library, and Stimulus for JavaScript. To implement the dropdown, we adapted the ComboBox component from RubyUI to meet our needs. The result was a game-changer for managing teams and accounts in our system.
One of the key features was enabling users to search teams and accounts quickly and independently on the client side. We designed an interactive UI that dynamically hides teams with no relevant accounts as the user types in the accounts search box. This enhancement significantly improved system management on our end and provided a seamless experience for customers with multiple teams and a large number of accounts.
Generating Unique Team Avatars
Another challenge was creating unique random team avatars. Initially, I tried generating two random colors for each team and rendering a gradient circle. However, the results were visually unappealing.
After some research, I found Vercel’s Avatar project. I studied its implementation and re-created it as a Stimulus controller—and voilà! We had the avatars we needed.
Team Invitations
Next, I worked on the team invitation feature. I realized that team owners should send invitations, which should remain pending until the invited user accepts them.
To support this, I introduced a new Invitations model, where:
Each user and team can have multiple invitations.
Each invitation belongs to a single user (Who created the invitation) and a single team.

Users are invited by email, so they can be invited even if they have not signed up to our platform before.
After implementing this change and fixing a few minor bugs, we successfully launched the feature.
Final Thoughts
We built the entire teams feature in-house, tailored it to our needs, and kept complete control over our customer data—without relying on third-party providers.
Building your own solution isn’t always a bad idea. As our experience shows, it can provide flexibility, control, and long-term value when done thoughtfully. 🚀
Wriitten by