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 workspace
  • member – Foreign key to the User model
  • role – 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 reference
  • email – Invitee email address
  • token – Secure unique token for invitation validation
  • accepted – Boolean indicating acceptance status
  • role – 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:

  1. Retrieves the inviter, workspace, and WorkspaceMemberInvite record
  2. Generates a secure invitation link containing the token
  3. Renders the emails/invitations/workspace_invitation.html template
  4. 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 a POST request to /api/workspaces/{slug}/invitations/ with bulk invite data, creating WorkspaceMemberInvite records and triggering the Celery email task
  • workspaceInvitations – Retrieves pending invitations via GET /api/workspaces/{slug}/invitations/
  • updateWorkspaceInvitation – Modifies invitation properties via PATCH /api/workspaces/{slug}/invitations/{id}/
  • deleteWorkspaceInvitations – Revokes invitations via DELETE /api/workspaces/{slug}/invitations/{id}/

Member Management Endpoints

Active member operations follow standard REST patterns:

  • fetchWorkspaceMembers – Retrieves all members via GET /api/workspaces/{slug}/members/
  • updateWorkspaceMember – Changes member roles via PATCH /api/workspaces/{slug}/members/{id}/
  • deleteWorkspaceMember – Deactivates members via DELETE /api/workspaces/{slug}/members/{id}/
  • joinWorkspace – Accepts invitations via POST /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 list
  • updateMember – Performs optimistic role updates, rolling back on API failure
  • Invitation acceptance – Updates local maps after joinWorkspace succeeds, 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 WorkspaceMember for active users and WorkspaceMemberInvite for pending invitations, both inheriting soft-delete capabilities from BaseModel in apps/api/plane/db/models/workspace.py.

  • Asynchronous Processing: The workspace_invitation Celery task in apps/api/plane/bgtasks/workspace_invitation_task.py handles 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 the WorkspaceService class in apps/web/core/services/workspace.service.ts.

  • State Management: The WorkspaceMemberStore in apps/web/core/store/member/workspace/workspace-member.store.ts uses 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:

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 →