Workspace-Level Labels and Estimates Architecture in Plane: Backend and Frontend Deep Dive
Plane implements workspace-level labels and estimates through a layered architecture where Django models with workspace ForeignKeys persist data, REST APIs expose CRUD endpoints, and React/MobX stores cache and compute hierarchical views for the UI.
Plane (makeplane/plane) treats labels and estimates as first-class workspace resources that can be shared across projects or scoped individually. This article examines the complete architecture for workspace-level labels and estimates, tracing the data flow from PostgreSQL tables through Django ORM models to the TypeScript service layer and MobX stores that power the React frontend.
Backend Architecture for Workspace Resources
The backend implements workspace-level labels and estimates using Django models that inherit from WorkspaceBaseModel, enforcing data integrity through foreign key relationships and unique constraints at the database level.
Label Models and Hierarchy
Labels in Plane support both workspace-wide and project-specific scopes, with optional parent-child relationships for categorization.
The Label model is defined in apps/api/plane/db/models/label.py. It inherits from WorkspaceBaseModel, which automatically links every label to a workspace via workspace_id. Labels can optionally belong to a specific project through the project_id field, or exist at the workspace level when project_id is null.
Key model features include:
- Hierarchy support: The
parentfield is a self-referencing foreign key that enables nested label categories - Custom ordering: The
sort_orderfield is auto-incremented on creation via thesave()method to support drag-and-drop reordering - Uniqueness constraints: The database enforces unique
namevalues per workspace or per project when records are not soft-deleted
# Conceptual model structure from apps/api/plane/db/models/label.py
class Label(WorkspaceBaseModel):
name = models.CharField(max_length=255)
color = models.CharField(max_length=255, blank=True)
parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True)
sort_order = models.FloatField(default=0)
project = models.ForeignKey("db.Project", on_delete=models.CASCADE, null=True, blank=True)
# workspace inherited from WorkspaceBaseModel
Estimate Models and Point Systems
Estimates in Plane are technically owned by projects but exposed at the workspace level through aggregation endpoints. The architecture separates the estimate definition from its individual point values.
In apps/api/plane/db/models/estimate.py, the Estimate model links to a project via project_id and includes metadata fields:
type: Eithercategoriesorpointsdepending on the estimation methodlast_used: Boolean flag identifying the currently active estimate for the project
Individual estimation points are stored in the EstimatePoint model, which links to an Estimate via foreign key and stores the display value, numeric key, and optional description.
The Project model (apps/api/plane/db/models/project.py) maintains an optional foreign key estimate pointing to the default estimate for that project, enabling quick lookup of the active estimation system.
# From apps/api/plane/db/models/estimate.py
class EstimatePoint(models.Model):
estimate = models.ForeignKey("db.Estimate", on_delete=models.CASCADE, related_name="points")
key = models.IntegerField(default=0, validators=[MinValueValidator(0)])
value = models.CharField(max_length=255)
description = models.TextField(blank=True)
REST API Endpoints
Plane exposes workspace-level labels and estimates through dedicated view modules that handle serialization and permission checks.
Label Endpoints:
apps/api/plane/app/views/workspace/label.py: HandlesGET /api/workspaces/:slug/labels/for workspace-wide label CRUDapps/api/plane/app/views/issue/label.py: ManagesGET /api/workspaces/:slug/projects/:id/issue-labels/for project-specific labelsapps/api/plane/app/serializers/label.py: SerializesLabelinstances for API responses
Estimate Endpoints:
apps/api/plane/app/views/workspace/estimate.py: Aggregates all estimates across a workspace viaGET /api/workspaces/:slug/estimates/apps/api/plane/app/views/project/estimate.py: Handles project-specific estimate managementapps/api/plane/app/serializers/estimate.py: Serializes bothEstimateandEstimatePointobjects
Frontend State Management
The frontend architecture uses MobX stores to cache workspace-level labels and estimates, providing computed getters that transform flat API responses into hierarchical structures suitable for UI components.
LabelStore and Hierarchical Data
The LabelStore class in apps/web/core/store/label.store.ts serves as the single source of truth for label data across the application.
The store maintains a labelMap (observable dictionary keyed by label ID) and provides computed getters that derive specific views:
workspaceLabels: Returns all labels whereproject_idis nullprojectLabels(projectId): Filters labels by specific projectprojectLabelsTree: Usesutils/buildTree.tsfrom@plane/utilsto convert the flat label list into a hierarchical tree structure based onparentrelationships
Data fetching flows through the service layer:
- Components call
labelStore.fetchWorkspaceLabels(workspaceSlug) - The store invokes
IssueLabelService.getWorkspaceIssueLabels(slug)fromapps/web/core/services/issue/issue_label.service.ts - The service performs the HTTP GET to
/api/workspaces/:slug/labels/ - Response data populates the
labelMap, triggering reactive UI updates
For drag-and-drop reordering, the store provides updateLabelPosition(), which calculates new sort_order values based on neighboring labels and persists changes via IssueLabelService.patchIssueLabel().
ProjectEstimateStore for Active Estimates
Estimates are managed by ProjectEstimateStore in apps/web/core/store/estimates/project-estimate.store.ts, which handles the complexity of tracking active versus archived estimates.
The store caches estimates in an estimates map and exposes computed properties:
currentActiveEstimate: Returns the estimate wherelast_used === truefor the current projectcurrentActiveEstimateId: Returns the ID of the active estimatearchivedEstimateIds: Lists estimates that are no longer activeareEstimateEnabledByProjectId: Boolean check for estimate availability
The store wraps EstimateService (located at apps/web/core/services/estimate.service.ts) to fetch data from /api/workspaces/:slug/estimates/ and /api/workspaces/:slug/projects/:id/estimates/.
Practical Implementation Examples
Fetching Workspace-Level Labels
Components access workspace labels through the root store context, triggering fetches only when data is not cached:
import { useRootStore } from "@/store/context";
const LabelDropdown = ({ workspaceSlug }: { workspaceSlug: string }) => {
const { labelStore } = useRootStore();
useEffect(() => {
if (!labelStore.workspaceLabels) {
labelStore.fetchWorkspaceLabels(workspaceSlug);
}
}, [workspaceSlug, labelStore]);
const labels = labelStore.workspaceLabels ?? [];
return (
<Select>
{labels.map((label) => (
<Option key={label.id} value={label.id}>
{label.name}
</Option>
))}
</Select>
);
};
Updating Label Hierarchy
Drag-and-drop reordering updates both parent relationships and sort order:
await labelStore.updateLabelPosition(
workspaceSlug,
projectId,
draggedLabelId,
newParentId, // null for top-level placement
targetLabelId, // label to insert after
false // do not force drop at end
);
Accessing Active Estimates
UI components determine which estimate system to display using computed store properties:
const activeEstimate = projectEstimateStore.currentActiveEstimate;
if (activeEstimate) {
console.log("Active estimate:", activeEstimate.name, activeEstimate.type);
// Access points via activeEstimate.points
}
Summary
- Workspace-level labels are stored in the
Labelmodel (apps/api/plane/db/models/label.py) with optional project scoping and hierarchical parent relationships, exposed viaapps/api/plane/app/views/workspace/label.py. - Estimates are project-owned but workspace-accessible through aggregation endpoints in
apps/api/plane/app/views/workspace/estimate.py, with points stored separately in theEstimatePointmodel. - Frontend caching uses MobX stores (
LabelStoreandProjectEstimateStore) to maintain local state, compute hierarchical trees, and track active estimates via thelast_usedflag. - Service layer abstraction in
IssueLabelServiceandEstimateServicedecouples React components from direct API calls, enabling optimistic updates and error rollback. - Ordering and hierarchy are handled through database-level
sort_orderfields and runtime tree-building utilities that transform flat API responses into nested UI structures.
Frequently Asked Questions
What is the difference between workspace-level and project-level labels in Plane?
Workspace-level labels have a null project_id and are available across all projects within that workspace, while project-level labels have a specific project_id foreign key and are only visible within that project. Both types store their workspace association via WorkspaceBaseModel inheritance, but the LabelStore frontend computes separate workspaceLabels and projectLabels getters to filter the cached labelMap accordingly.
How does Plane handle label hierarchy and ordering?
Labels support arbitrary nesting through a self-referencing parent foreign key in the database model. The frontend uses utils/buildTree.ts to convert the flat list returned by the API into a hierarchical tree structure for display. Ordering is maintained via a sort_order float field; when users drag and drop labels, the updateLabelPosition method in LabelStore recalculates sort values based on neighboring labels and persists the change via PATCH requests to the label API.
Can estimates be shared across multiple projects in a workspace?
No, estimates are owned by individual projects through a project_id foreign key in the Estimate model. However, the workspace-level API endpoint (GET /api/workspaces/:slug/estimates/) aggregates all estimates from all projects within the workspace, allowing users to browse and select from existing estimates when configuring new projects. The Project model tracks its currently selected default estimate via an estimate foreign key.
How does the frontend determine which estimate is currently active?
The ProjectEstimateStore exposes a computed currentActiveEstimate getter that filters the cached estimates map to return the estimate where last_used === true. When a user selects a different estimate for a project, the backend updates the last_used flag on the Estimate model, and the store refreshes its local cache via getProjectEstimates(), triggering reactive updates in components like the estimate dropdown selector.
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 →