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: A SlugField with unique=True validated against restricted terms, serving as the URL-safe identifier
  • owner: Foreign key to settings.AUTH_USER_MODEL with CASCADE deletion and related_name='owner_workspace'
  • logo_asset: Optional foreign key to db.FileAsset using SET_NULL for asset-based branding
  • timezone: CharField defaulting to UTC via TIMEZONE_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 to db.Workspace with CASCADE deletion and related_name='workspace_project'
  • identifier: CharField(max_length=12, db_index=True) serving as a short unique key scoped to the parent workspace
  • network: PositiveSmallIntegerField controlling visibility (Public=2, Secret=0)
  • default_state: Foreign key to db.State using SET_NULL for workflow initialization
  • estimate: Optional foreign key to db.Estimate for 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 to db.State using SET_NULL, representing the current workflow position
  • priority: PositiveSmallIntegerField with choices ranging from Urgent to Low
  • assignee: Foreign key to the user model with SET_NULL and related_name='assigned_issues'
  • archived_at: DateTimeField for soft-archiving distinct from deletion
  • description_text / description_html: JSONField storing 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 between Issue and Label
  • IssueComment: Threaded discussions with comment text and created_by foreign key
  • IssueAssignee: Supports multiple assignees beyond the primary assignee field
  • IssueRelation: Tracks dependencies and blocking relationships between issues
  • IssueVersion: Implements field-level versioning with version_number tracking

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 slug validation and ownership via owner foreign key in apps/api/plane/db/models/workspace.py.
  • Project scopes work within a workspace using identifier uniqueness constraints and auto-inherits timezones via custom save() logic in apps/api/plane/db/models/project.py.
  • Issue extends ProjectBaseModel to 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_at timestamps 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:

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 →