# Django Database Schema Architecture for Workspaces, Projects, and Issues in Plane

> Discover the Django database schema architecture for Workspaces, Projects, and Issues in Plane. Learn how it ensures data integrity with abstract models and composite constraints.

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

---

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

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

```python

# Direct workspace-scoped query without Project JOIN

issues = Issue.objects.filter(workspace=ws, deleted_at__isnull=True)

```

**Project to Issues:**

```python

# Via related_name from ProjectBaseModel

project_issues = project.project_issue.all()

```

**Workspace to Projects:**

```python

# 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

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

```python

# 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

```python

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