How Plane Implements Workspace Invitation and Member Management: Backend Models, Celery Tasks, and MobX State
Plane implements workspace invitation and member management through a layered architecture using Django models for persistence, Celery for asynchronous email delivery, RESTful API endpoints for CRUD operations, and MobX stores for reactive frontend state management.
Plane is an open-source project management platform that handles workspace collaboration through a robust invitation system. Understanding how Plane manages workspace invitations and member roles requires examining the interaction between Django backend models, asynchronous task queues, and the TypeScript frontend stores. This analysis reveals the complete lifecycle from invitation creation to member activation.
Backend Data Models for Workspace Membership
Plane’s persistence layer defines two primary models in apps/api/plane/db/models/workspace.py that handle the membership lifecycle. Both models inherit from BaseModel, which provides soft-delete functionality.
WorkspaceMember and WorkspaceMemberInvite
The WorkspaceMember model represents active users belonging to a workspace. It contains foreign keys to both the Workspace and User models, along with role and status fields:
workspace– Foreign key to the workspacemember– Foreign key to theUsermodelrole– Role assignment (e.g., admin, member, guest)is_active– Boolean flag for soft-delete status
The WorkspaceMemberInvite model stores pending invitations before users join. It tracks the invitation state through these key fields:
workspace– Target workspace referenceemail– Invitee email addresstoken– Secure unique token for invitation validationaccepted– Boolean indicating acceptance statusrole– Assigned role upon acceptance
Asynchronous Invitation Email Delivery with Celery
When an admin creates an invitation, Plane immediately enqueues a background task to handle email delivery. This prevents HTTP request blocking and ensures reliable delivery.
The workspace_invitation task in apps/api/plane/bgtasks/workspace_invitation_task.py performs the following operations:
- Retrieves the inviter, workspace, and
WorkspaceMemberInviterecord - Generates a secure invitation link containing the token
- Renders the
emails/invitations/workspace_invitation.htmltemplate - Sends the email via the configured backend
The generated invitation URL follows this pattern:
/workspace-invitations/?invitation_id=<invite.id>&slug=<workspace.slug>&token=<token>
This token-based approach ensures that only recipients with access to the email can accept the invitation.
REST API Endpoints and Service Layer
All workspace membership operations are exposed through the /api/workspaces/{slug}/ namespace. The frontend WorkspaceService in apps/web/core/services/workspace.service.ts wraps these endpoints with TypeScript methods.
Invitation Management Endpoints
The invitation lifecycle uses these HTTP endpoints:
inviteWorkspace– Sends aPOSTrequest to/api/workspaces/{slug}/invitations/with bulk invite data, creatingWorkspaceMemberInviterecords and triggering the Celery email taskworkspaceInvitations– Retrieves pending invitations viaGET /api/workspaces/{slug}/invitations/updateWorkspaceInvitation– Modifies invitation properties viaPATCH /api/workspaces/{slug}/invitations/{id}/deleteWorkspaceInvitations– Revokes invitations viaDELETE /api/workspaces/{slug}/invitations/{id}/
Member Management Endpoints
Active member operations follow standard REST patterns:
fetchWorkspaceMembers– Retrieves all members viaGET /api/workspaces/{slug}/members/updateWorkspaceMember– Changes member roles viaPATCH /api/workspaces/{slug}/members/{id}/deleteWorkspaceMember– Deactivates members viaDELETE /api/workspaces/{slug}/members/{id}/joinWorkspace– Accepts invitations viaPOST /api/workspaces/{slug}/invitations/{invitation_id}/join/
The service implementation in workspace.service.ts handles the HTTP layer:
async inviteWorkspace(workspaceSlug: string, data: IWorkspaceBulkInviteFormData) {
return this.post(`/api/workspaces/${workspaceSlug}/invitations/`, data)
.then(r => r?.data)
.catch(e => { throw e?.response?.data });
}
Frontend State Management with MobX
The UI layer manages membership state through the WorkspaceMemberStore in apps/web/core/store/member/workspace/workspace-member.store.ts. This MobX store maintains reactive maps of members and invitations, providing computed selectors and optimistic updates.
Fetching and Storing Members
The fetchWorkspaceMembers method populates the workspaceMemberMap with normalized data:
await this.workspaceService.fetchWorkspaceMembers(workspaceSlug).then(response => {
runInAction(() => {
response.forEach(member => {
set(this.memberRoot?.memberMap, member.member.id, { ...member.member, joining_date: member.created_at });
set(this.workspaceMemberMap, [workspaceSlug, member.member.id], {
id: member.id,
member: member.member.id,
role: member.role,
is_active: member.is_active,
});
});
});
});
This normalization allows O(1) lookups for member details across the application.
Handling Invitations and Optimistic Updates
The store provides actions that mirror the service layer while managing local state:
inviteMembersToWorkspace– Sends bulk invites through the service, then refreshes the invitation listupdateMember– Performs optimistic role updates, rolling back on API failure- Invitation acceptance – Updates local maps after
joinWorkspacesucceeds, converting invites to active members
Role-based permissions from @plane/constants (e.g., EUserPermissions.ADMIN, MEMBER, GUEST) gate these operations in the UI.
Practical Implementation Examples
Sending Bulk Invitations
import { useCallback } from "react";
import { WorkspaceMemberStore } from "@/store/member/workspace-member.store";
function InviteButton({ workspaceSlug }: { workspaceSlug: string }) {
const memberStore = useRootStore().member.workspaceMember;
const handleInvite = useCallback(async (emails: string[]) => {
const data = {
emails,
role: "member", // or "admin"
message: "Join us on Plane!",
};
await memberStore.inviteMembersToWorkspace(workspaceSlug, data);
// Store automatically refreshes workspaceMemberInvitations
}, [memberStore, workspaceSlug]);
return <button onClick={() => handleInvite(["user@example.com"])}>Invite</button>;
}
Accepting an Invitation
import { WorkspaceService } from "@/services/workspace.service";
async function acceptInvitation(
workspaceSlug: string,
invitationId: string,
token: string
) {
const payload = { token };
await new WorkspaceService().joinWorkspace(workspaceSlug, invitationId, payload);
// On success, the backend creates a WorkspaceMember and marks invite as accepted
}
Updating Member Roles
// Optimistic update with automatic rollback on failure
await memberStore.updateMember(workspaceSlug, userId, { role: "admin" });
Accessing Member Data
// Computed property returns array of member IDs for current workspace
const memberIds = memberStore.getWorkspaceMemberIds;
const members = memberIds?.map(id => memberStore.getWorkspaceMemberDetails(id));
Summary
-
Data Layer: Plane uses
WorkspaceMemberfor active users andWorkspaceMemberInvitefor pending invitations, both inheriting soft-delete capabilities fromBaseModelinapps/api/plane/db/models/workspace.py. -
Asynchronous Processing: The
workspace_invitationCelery task inapps/api/plane/bgtasks/workspace_invitation_task.pyhandles email delivery outside the request cycle, generating tokenized links for secure acceptance. -
API Architecture: REST endpoints under
/api/workspaces/{slug}/provide CRUD operations for invitations and members, wrapped by theWorkspaceServiceclass inapps/web/core/services/workspace.service.ts. -
State Management: The
WorkspaceMemberStoreinapps/web/core/store/member/workspace/workspace-member.store.tsuses MobX to maintain normalized member maps, handle optimistic updates, and synchronize UI state with backend changes. -
Security: Token-based invitation URLs ensure only intended recipients can join workspaces, while role-based permissions enforce administrative controls over member management operations.
Frequently Asked Questions
How does Plane secure invitation tokens?
Plane generates unique tokens stored in the WorkspaceMemberInvite model. These tokens are included in acceptance URLs sent via email. When a user accepts an invitation through the joinWorkspace endpoint, the backend validates this token against the invitation record before creating the WorkspaceMember entry, ensuring only authenticated email recipients can complete the join process.
What happens when a user accepts a workspace invitation?
The frontend calls WorkspaceService.joinWorkspace, which sends a POST request to /api/workspaces/{slug}/invitations/{invitation_id}/join/ with the validation token. The backend creates a new WorkspaceMember record linking the user to the workspace, sets the accepted field to True on the WorkspaceMemberInvite record, and returns the new member data. The frontend store then updates its workspaceMemberMap to reflect the new active member.
How does Plane handle role-based permissions for member management?
Plane defines permission constants in @plane/constants including EUserPermissions.ADMIN, MEMBER, and GUEST. The backend enforces these through permission classes in apps/api/plane/app/permissions/workspace.py, while the frontend MobX store checks these roles before allowing updateMember or deleteWorkspaceMember operations. Only users with sufficient privileges can modify roles or revoke memberships.
Can workspace invitations be revoked before acceptance?
Yes. Administrators can call deleteWorkspaceInvitations on the service layer or invoke the corresponding action in the WorkspaceMemberStore, which sends a DELETE request to /api/workspaces/{slug}/invitations/{id}/. This removes the WorkspaceMemberInvite record from the database, invalidating the token and preventing the invitee from joining the workspace.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →