From an email address to a teammate

Building AI Drive's team invitations, and keeping the invitation intact through email, sign-in, and the decision to join.

Inviting someone to a team starts with a small action: enter an email address and choose a role. In AI Drive, I worked across the backend and frontend to turn that action into an invitation the recipient could open, return to after signing in, and accept.

The backend work landed in October 2025. The frontend followed in December, including the invitation page, pending invitations, and the connections to signup and login. Later fixes took me back into the same flow when a redirect closed the invitation dialog and when an expired invitation prevented an admin from sending a new one.

Those problems explain the feature better than a screenshot of the Add Member button. An invitation crosses several parts of the product, often with time between them. Each part has to preserve enough information for the next person, or the next page, to finish the work.

Starting before the person had an account

AI Drive already had teams and a way to add existing users. Email invitations needed to work earlier in that relationship. An admin should be able to invite a colleague who had not created an AI Drive account yet, without asking them to sign up first and then repeating the team setup.

I added a stored invitation that connected the team, recipient email, inviter, and intended role. It also carried a random token, an expiry time, and a status. That gave the email link something the backend could look up when the recipient arrived later. The invitation could exist before the account that would eventually accept it.

Accepting still required a signed-in account whose email matched the invitation. Possessing the link was not enough to become the invited teammate. The backend also checked the invitation's status and expiry, and whether the person already belonged to a team. The role came from the stored invitation rather than a choice supplied by the acceptance page.

Giving the admin something to come back to

Sending an invitation did not make the recipient a member. That distinction needed to be visible in the interface too. I added a pending-invitations section alongside team management so admins could see the work that was still waiting for someone else.

From there, an admin could resend or cancel an invitation. Resending extended its expiry, with a limit on repeated resends. Cancellation changed the stored state so the link could no longer be used to accept that invitation. Those actions needed their own loading and error handling; clicking a button could not be treated as proof that the server had completed the change.

The first version also counted pending invitations when checking available team seats. Someone who had been invited and someone who had joined were different states, but both mattered to whether the admin could invite another person. Later, I updated the seat checks for Enterprise teams, where the billed-seat constraint did not apply. A rule that made sense for one kind of team could not simply be carried into every plan.

Getting through sign-in without losing the task

The recipient's path was longer than the admin's. They opened an email link, but might already be signed in, need to log in, or need to create an account. All three paths had to lead back to the same invitation.

I added an invitation acceptance page and carried its destination through the authentication screens. The page waited for the auth state to be resolved before deciding whether to send someone to signup or show the invitation. An account that had not loaded yet could otherwise be mistaken for a signed-out visitor.

The invitation stays the destination

  1. Open the invitation
  2. Sign in or create an account, if needed
  3. Return to the invitation and choose whether to join
Authentication is one step in the task. It should preserve what the person came to do.

That return destination also needed validation. A redirect parameter comes from the URL, so I added a helper that allowed local paths and rejected external destinations and malformed values. The flow needed to remember where the person was going without accepting an arbitrary destination just because it arrived with a login request.

The dialog appeared, then disappeared

One bug made the handoff especially clear. A logged-out recipient or a new user could follow an invitation, sign up, and briefly see the acceptance dialog. Then it closed without them doing anything.

The invitation page was not the only part of the application making navigation decisions. The existing authentication flow normally sent signed-in users away from auth pages to Home. That was useful for an ordinary visit, but it could override the invitation they were still trying to accept.

I changed the routing logic to recognise an active invitation return path and let the client complete that flow. The acceptance page also tracked the intent to show the dialog across auth updates. The important condition was whether the person had finished with the invitation, rather than whether their authentication state had just changed.

This changed how I thought about testing the feature. Opening the acceptance page while already signed in covered only one path. The signup-to-invitation journey exercised the interaction between routing, auth state, and the dialog, which was where this bug lived.

An expired invitation was still blocking a new one

Another problem surfaced later. An admin could see an invitation as expired and try to invite the same person again, only to be told an invitation was already pending.

Both results came from the same record. The interface could compare the expiry time with the current time and display an expired invitation. In storage, however, its status could still be pending. The earlier backend flow updated that status when someone tried to accept an expired link. If nobody opened it, that update might never happen.

The duplicate-invitation check looked at the pending status and blocked the new invitation. In February 2026, I changed that check to inspect the expiry time as well. When the existing invitation was stale, the backend marked it expired and allowed the new invitation to proceed. A live pending invitation still blocked a duplicate.

The lesson was about time as much as state. A stored status does not change just because its deadline passes. Any operation that depends on whether an invitation is still usable has to account for that deadline, including the operation that creates its replacement.

The email was part of the feature

I also built the invitation email templates and delivery integration, along with a notification to the admins when someone accepted. The email needed to explain which team was inviting the recipient, what role they were being offered, and when the invitation would expire. Those details connected the message in their inbox to the decision they would make in the application.

Review led to another correction: I enabled automatic HTML escaping in the email templates. Names supplied to the template needed to be treated as text when rendered into an HTML message. The same care used for content on a web page had to extend to the email that brought someone there.

The work included tests around invitation rules, routes, email rendering, and frontend service calls and redirect validation. The feature initially stayed behind an internal frontend gate. After the redirect fix, I removed that gate in December 2025 so the email invitation flow was available to all team admins.

What building both ends taught me

Working on both sides made the gaps easier to see. A valid backend invitation could still be unreachable after signup. A useful expired label could still disagree with the check that blocked a replacement. A delivered email could start a journey that the rest of the application did not yet preserve.

Teams and authentication already existed, and colleagues continued to extend the flow afterward. My part was connecting the invitation lifecycle, email delivery, management interface, and acceptance journey, then following the problems that appeared between them.

The feature began with an email address and a role. Making it useful meant keeping that intent intact until the recipient could decide to become a teammate. That is the part of the work I would carry into another onboarding flow: follow the task through every handoff, including the ones that happen after the first screen has worked.