How Team Collaboration and Invitations Are Implemented in the Developer Roadmap App

The team collaboration feature uses nanostores for reactive state management, React components for UI flows, and REST-style endpoints for creating, fetching, and responding to invitations.

The kamranahmedse/developer-roadmap repository implements a complete team collaboration system that allows users to create teams, invite members via email, and manage roles through a permission-based interface. This implementation spans client-side state management, React component architecture, and HTTP API integration to deliver a seamless invitation lifecycle from creation to acceptance.

State Management with Nanostores

The foundation of the team collaboration feature lives in src/stores/team.ts, which uses nanostores to provide reactive state across the application. The store defines several key atoms that track the user's team memberships and permissions.

export const $teamList = atom<UserTeamItem[]>([]);
export const $currentTeam = atom<UserTeamItem | undefined>();
export const $currentTeamRole = computed($currentTeam, t => t?.role);
export const $isCurrentTeamAdmin = computed($currentTeamRole, r => ['admin'].includes(r!));
export const $canManageCurrentTeam = computed($currentTeamRole, r => ['admin','manager'].includes(r!));

These atoms enable any component to subscribe to team data and role permissions without prop drilling. The $teamList atom holds all teams where the user has membership, while computed atoms like $isCurrentTeamAdmin and $canManageCurrentTeam derive boolean permissions used to conditionally render invitation controls.

Creating and Managing Teams

Team creation begins in src/components/CreateTeam/CreateTeamForm.tsx, which handles the initial team document creation. After a successful creation, the component fetches the new team details to populate the store.

const { response, error } = await httpGet<TeamDocument>(`${API}/v1-get-team/${teamId}`);

Once a team exists in $teamList, the invitation functionality becomes available to users with appropriate permissions. The TeamsList.tsx component reads from $teamList to display teams and filters entries based on team.status to show pending invitations separately from active memberships.

The Invitation Flow

The invitation system follows a complete lifecycle through distinct API endpoints, handled by specialized React components that manage each phase of the process.

Sending Invitations

The InviteMemberPopup.tsx component manages the invitation creation interface. It collects an email address and role selection (via RoleDropdown.tsx), then submits the invitation through a POST request.

const { response, error } = await httpPost(
  `${import.meta.env.PUBLIC_API_URL}/v1-invite-member/${teamId}`,
  { email, role: selectedRole },
);

The request flows through the generic httpPost wrapper in src/lib/http.ts, which automatically injects authentication tokens and fingerprint headers. The backend creates an invitation record and emails a link formatted as /respond-invite?i=<inviteId> to the recipient.

Responding to Invitations

When an invitee clicks the email link, RespondInviteForm.tsx handles the acceptance or rejection workflow. The component first extracts the invitation ID from the URL parameters.

const { i: inviteId } = getUrlParams();

It then fetches invitation details using a GET request to validate the invitation before displaying action buttons.

const { response, error } = await httpGet<InvitationResponse>(
  `${import.meta.env.PUBLIC_API_URL}/v1-get-invitation/${inviteId}`,
);

Upon user action, the component sends a PATCH request to record the decision.

const { response, error } = await httpPatch<{ teamId: string }>(
  `${import.meta.env.PUBLIC_API_URL}/v1-respond-invite/${inviteId}`,
  { status: 'accept' | 'reject' },
);

Successful acceptance redirects the user to /teams/${teamId}/activity, while rejection routes to the home page. The backend updates the member's status from invited to joined, which reflects in the client's $teamList on the next data fetch.

Permission-Based UI Rendering

Components throughout the application use the nanostore computed atoms to gate invitation functionality. In DashboardPage.tsx and team list views, the system checks $isCurrentTeamAdmin or $canManageCurrentTeam before rendering invite buttons or management controls.

import { $currentTeam, $isCurrentTeamAdmin } from '@/stores/team';

const isAdmin = $currentTeam.get()?._id === team._id && $isCurrentTeamAdmin.get();

Only users with admin or manager roles see the invitation interface, ensuring proper access control without additional server round-trips for permission checks.

Summary

  • State management relies on nanostores in src/stores/team.ts to track $teamList, $currentTeam, and computed role permissions like $isCurrentTeamAdmin.
  • Team creation occurs through CreateTeamForm.tsx, which validates the new team via httpGet before enabling invitation features.
  • Invitations are sent via POST /v1-invite-member/:teamId from InviteMemberPopup.tsx, with the backend generating email links containing unique invitation IDs.
  • Invitation responses are handled by RespondInviteForm.tsx using GET /v1-get-invitation/:inviteId to fetch details and PATCH /v1-respond-invite/:inviteId to accept or reject.
  • Permission checks use reactive atoms to conditionally render UI elements based on the user's role (admin, manager, or member) in the current team context.

Frequently Asked Questions

How does the application track which users can invite new team members?

The application uses computed atoms in src/stores/team.ts called $isCurrentTeamAdmin and $canManageCurrentTeam. These check if the user's role in $currentTeam is admin or manager. React components subscribe to these atoms and only render invitation buttons when the user has the required permissions, eliminating unnecessary API calls for unauthorized actions.

What API endpoints handle the invitation lifecycle?

The system uses three main REST-style endpoints: POST /v1-invite-member/:teamId to create invitations, GET /v1-get-invitation/:inviteId to fetch invitation details when a user clicks an email link, and PATCH /v1-respond-invite/:inviteId to process acceptance or rejection responses. All requests flow through the generic HTTP wrappers in src/lib/http.ts which handle authentication and error formatting.

How does the invitee's team status update from "invited" to "joined"?

When an invitee accepts via RespondInviteForm.tsx, the PATCH request updates the server-side status. The client-side $teamList atom refreshes on the next data fetch (either through automatic sync or manual refresh), replacing the status: 'invited' entry with status: 'joined' or removing rejected invitations entirely. The TeamsList.tsx component reacts to these state changes to update the UI badges and navigation links.

Can invitations be sent programmatically outside the React UI?

Yes, the same httpPost utility used in InviteMemberPopup.tsx can be imported from src/lib/http.ts and used in server-side scripts or service layers. You would call the v1-invite-member endpoint with the team ID, email, and role parameters, passing the same authentication headers required by the client implementation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →