Django Database Schema Architecture for Workspaces, Projects, and Issues in Plane
Plane implements a hierarchical Django database schema where Workspaces own Projects, Projects contain Issues, and all three layers utilize abstract base models, soft-deletion timestamps, and composite unique constraints to enforce data integrity.
The open-source project management platform Plane structures its core collaborative entities through a tightly coupled Django ORM architecture defined in apps/api/plane/db/models/. This Django database schema architecture for Workspaces, Projects, and Issues relies on foreign-key cascading, workspace-aware abstract mixins, and unique constraints that incorporate soft-deletion states to prevent data collisions while supporting recovery workflows.
Workspace Model Schema
Source: apps/api/plane/db/models/workspace.py
The Workspace model serves as the root tenant container. It defines the top-level namespace where all subsequent project data resides.
Core Fields and Relationships
The model establishes ownership and identification through several critical fields:
slug: ASlugFieldwithunique=Truevalidated against restricted terms, serving as the URL-safe identifierowner: Foreign key tosettings.AUTH_USER_MODELwithCASCADEdeletion andrelated_name='owner_workspace'logo_asset: Optional foreign key todb.FileAssetusingSET_NULLfor asset-based brandingtimezone:CharFielddefaulting to UTC viaTIMEZONE_CHOICES
The WorkspaceBaseModel abstract class (defined in the same file) provides reusable scaffolding for any model requiring workspace scope. It automatically adds a workspace foreign key and optional project field, enabling downstream models to inherit proper scoping without redundant field declarations.
Constraints and Soft-Deletion
Workspace membership enforces uniqueness through composite constraints. The WorkspaceMember model (extending WorkspaceBaseModel) defines unique_together = ["workspace", "member", "deleted_at"], ensuring a user appears only once per workspace unless soft-deleted.
Soft-deletion operates through the shared BaseModel mixin. When delete() is called on a workspace, the implementation appends a timestamp to the slug field before saving, freeing the original slug for reuse while preserving referential integrity in related tables.
Project Model Schema
Source: apps/api/plane/db/models/project.py
The Project model creates an isolated container within a workspace, implementing feature toggles, visual customization, and automatic timezone inheritance.
Project Fields and Workspace Inheritance
Key architectural fields include:
workspace: Foreign key todb.WorkspacewithCASCADEdeletion andrelated_name='workspace_project'identifier:CharField(max_length=12, db_index=True)serving as a short unique key scoped to the parent workspacenetwork:PositiveSmallIntegerFieldcontrolling visibility (Public=2, Secret=0)default_state: Foreign key todb.StateusingSET_NULLfor workflow initializationestimate: Optional foreign key todb.Estimatefor effort tracking
The ProjectBaseModel abstract mixin (lines 80-90) automatically propagates workspace context. Any model inheriting from ProjectBaseModel receives both project and workspace foreign keys, with the save() method auto-populating the workspace field from the parent project if not explicitly provided.
Timezone inheritance occurs in the model's save() method: if timezone is empty, the project copies the value from its parent workspace before persisting.
Unique Constraints and Identifiers
Plane enforces workspace-scoped uniqueness through explicit UniqueConstraint objects rather than simple unique_together. The schema uses:
constraints = [
UniqueConstraint(
fields=["identifier", "workspace", "deleted_at"],
name="unique_project_identifier_per_workspace",
condition=Q(deleted_at__isnull=True)
)
]
This approach allows duplicate identifiers for archived or deleted projects while preventing active collisions. A similar constraint applies to project names within a workspace.
Issue Model Schema
Source: apps/api/plane/db/models/issue.py
The Issue model represents the atomic work item, extending ProjectBaseModel to inherit both project and workspace foreign keys automatically.
Core Issue Fields and State Management
The model tracks work progression through:
state: Foreign key todb.StateusingSET_NULL, representing the current workflow positionpriority:PositiveSmallIntegerFieldwith choices ranging from Urgent to Lowassignee: Foreign key to the user model withSET_NULLandrelated_name='assigned_issues'archived_at:DateTimeFieldfor soft-archiving distinct from deletiondescription_text/description_html:JSONFieldstoring structured rich-text content
Because Issue inherits from ProjectBaseModel, it maintains direct foreign keys to both Project and Workspace. This denormalization allows efficient querying across the hierarchy without JOIN traversal: Issue.objects.filter(workspace=ws) executes without touching the Project table.
Sub-Models and Relationships
Plane decomposes issue metadata into satellite models, all extending ProjectBaseModel to maintain workspace scoping:
IssueLabel: Many-to-many junction betweenIssueandLabelIssueComment: Threaded discussions withcommenttext andcreated_byforeign keyIssueAssignee: Supports multiple assignees beyond the primaryassigneefieldIssueRelation: Tracks dependencies and blocking relationships between issuesIssueVersion: Implements field-level versioning withversion_numbertracking
Each sub-model implements soft-deletion-aware uniqueness. For example, IssueLabel likely enforces unique_together = ["issue", "label", "deleted_at"] to prevent duplicate label assignments while allowing historical recovery.
Hierarchical Data Flow and Query Patterns
The schema architecture enables efficient traversal across all three levels:
Workspace to Issues:
# Direct workspace-scoped query without Project JOIN
issues = Issue.objects.filter(workspace=ws, deleted_at__isnull=True)
Project to Issues:
# Via related_name from ProjectBaseModel
project_issues = project.project_issue.all()
Workspace to Projects:
# Related name from Workspace model
projects = workspace.workspace_project.filter(archived_at__isnull=True)
The ProjectBaseModel.save() method ensures workspace consistency. When creating an Issue or any sub-model (like IssueComment), the implementation automatically sets the workspace field by referencing self.project.workspace, preventing workspace/project mismatches at the database level.
Practical Implementation Examples
Creating the Hierarchy
from plane.db.models import Workspace, Project, Issue
# 1. Establish Workspace
workspace = Workspace.objects.create(
name="Engineering",
slug="engineering",
owner=admin_user,
timezone="America/New_York"
)
# 2. Create Project (timezone auto-inherited)
project = Project.objects.create(
name="Q4 Roadmap",
workspace=workspace,
identifier="Q4R",
default_assignee=admin_user,
network=2 # Public
)
# 3. Create Issue (workspace FK auto-populated via ProjectBaseModel)
issue = Issue.objects.create(
name="Migrate authentication service",
project=project,
priority=1, # High
state=backlog_state,
description_text={"type": "doc", "content": "Migration plan..."},
created_by=admin_user
)
Adding Metadata with Constraints
# Add label (enforces uniqueness per issue via deleted_at constraint)
label, _ = Label.objects.get_or_create(
name="backend",
workspace=workspace,
defaults={"color": "#FF0000"}
)
IssueLabel.objects.create(issue=issue, label=label)
# Add comment (inherits workspace from project via ProjectBaseModel)
comment = IssueComment.objects.create(
issue=issue,
comment="Need to evaluate OAuth providers",
created_by=admin_user
# workspace field auto-set to workspace.id
)
Soft-Deletion Handling
# Soft delete project (frees identifier for reuse)
project.delete() # Sets deleted_at and appends timestamp to identifier
# Archived projects remain queryable but excluded from active lists
active_projects = Project.objects.filter(
workspace=workspace,
deleted_at__isnull=True,
archived_at__isnull=True
)
Summary
- Workspace acts as the root tenant with unique
slugvalidation and ownership viaownerforeign key inapps/api/plane/db/models/workspace.py. - Project scopes work within a workspace using
identifieruniqueness constraints and auto-inherits timezones via customsave()logic inapps/api/plane/db/models/project.py. - Issue extends
ProjectBaseModelto maintain direct workspace and project foreign keys, enabling efficient cross-hierarchy queries without JOIN overhead. - Abstract mixins (
WorkspaceBaseModel,ProjectBaseModel) standardize foreign-key relationships and automatic field population across the schema. - Soft-deletion is implemented through
deleted_attimestamps and conditional unique constraints, allowing data recovery while preventing active-row collisions.
Frequently Asked Questions
How does Plane handle unique project identifiers within a workspace?
Plane enforces identifier uniqueness through Django UniqueConstraint objects that include deleted_at as a constraint field. In apps/api/plane/db/models/project.py, the constraint applies only when deleted_at__isnull=True, allowing deleted projects to retain their identifiers in archived form while freeing the identifier for new active projects. The identifier field is indexed (db_index=True) for performant lookups.
What is the purpose of ProjectBaseModel in the Issue schema?
ProjectBaseModel is an abstract mixin defined in apps/api/plane/db/models/project.py that provides standardized project and workspace foreign keys to all descendant models. When Issue or IssueComment calls save(), the mixin automatically populates the workspace field by reading self.project.workspace, ensuring referential integrity and enabling direct workspace-scoped queries on issue sub-tables without requiring JOINs through the Project table.
How does the Workspace model support soft-deletion without breaking foreign key relationships?
The Workspace model implements soft-deletion by overriding the delete() method to append a timestamp to the slug field before setting deleted_at. This preserves the database row and all dependent foreign keys (which remain valid because the row still exists), while freeing the unique slug for new workspace creation. Related entities like Projects and Issues remain linked to the soft-deleted workspace row until explicitly purged or cascade-deleted by database constraints.
Why does the Issue model store both project and workspace foreign keys?
The Issue model maintains direct foreign keys to both Project and Workspace (via ProjectBaseModel) to optimize query performance and enforce data integrity at the database level. This denormalization allows the system to execute Issue.objects.filter(workspace=ws) without joining through the Project table, and ensures that issue sub-models (like IssueLabel) automatically inherit correct workspace scoping through the abstract base model's save() method.
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 →