# How Plane Implements Workspace Invitation and Member Management: Backend Models, Celery Tasks, and MobX State

> Learn how Plane implements workspace invitation and member management using Django models, Celery tasks, and MobX for frontend state. Discover the backend architecture and reactive state management.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: internals
- Published: 2026-06-23

---

**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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/emails/invitations/workspace_invitation.html) template
4. Sends the email via the configured backend

The generated invitation URL follows this pattern:

```text
/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/workspace.service.ts) handles the HTTP layer:

```typescript
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`](https://github.com/makeplane/plane/blob/main/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:

```typescript
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

```typescript
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

```typescript
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

```typescript
// Optimistic update with automatic rollback on failure
await memberStore.updateMember(workspaceSlug, userId, { role: "admin" });

```

### Accessing Member Data

```typescript
// 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`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/workspace.py).

- **Asynchronous Processing**: The `workspace_invitation` Celery task in [`apps/api/plane/bgtasks/workspace_invitation_task.py`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/apps/web/core/services/workspace.service.ts).

- **State Management**: The `WorkspaceMemberStore` in [`apps/web/core/store/member/workspace/workspace-member.store.ts`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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.