How Plane Implements Issue Archiving with Soft-Delete and Restore Functionality
Plane uses a soft-delete pattern where the archived_at field on the Issue model acts as a deletion flag, allowing issues to be hidden from standard queries while preserving data for restoration and audit trails.
The open-source project management platform Plane (makeplane/plane) implements issue archiving through a sophisticated three-layer architecture that combines database-level filtering, state-aware API validation, and comprehensive activity logging. This approach ensures that archived issues remain accessible for restoration while being excluded from active workflows.
Database-Level Soft-Delete Implementation
Plane's soft-delete mechanism centers on a nullable date field and a custom model manager that automatically filters archived records from standard queries.
The archived_at Field
In apps/api/plane/db/models/issue.py, the Issue model defines an archived_at field as a nullable DateField:
# From apps/api/plane/db/models/issue.py (lines 58-61)
archived_at = models.DateField(
null=True,
blank=True,
help_text="Date when the issue was archived"
)
When this field contains null, the issue is considered active. When populated with a date, the issue enters an archived state. This preserves all relational data and issue history without physically deleting database rows.
IssueManager Filter Logic
The IssueManager custom manager automatically excludes archived issues from standard querysets. Located in the same file (lines 91-99), this manager overrides the default queryset to filter out rows where archived_at is not null:
# Conceptual implementation from IssueManager
class IssueManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(archived_at__isnull=True)
This ensures that any ORM query using Issue.objects or Issue.issue_objects (the default manager) automatically excludes archived issues, preventing accidental data leakage into active project views.
API Endpoints for Archive and Restore Operations
The archive functionality exposes dedicated REST endpoints in apps/api/plane/app/views/issue/archive.py, implementing strict state validation before allowing archival operations.
Single Issue Archiving with State Validation
The archive endpoint (POST /api/workspaces/{slug}/projects/{project_id}/archived-issues/) validates that an issue is in a terminal state before archiving. As implemented in lines 56-78, the system checks the issue.state.group property:
# Excerpt from apps/api/plane/app/views/issue/archive.py
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def archive(self, request, slug, project_id, pk=None):
issue = Issue.issue_objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
# State validation: only completed or cancelled issues can be archived
if issue.state.group not in ["completed", "cancelled"]:
return Response(
{"error": "Can only archive completed or cancelled state group issue"},
status=status.HTTP_400_BAD_REQUEST
)
# Set archive timestamp
issue.archived_at = timezone.now().date()
issue.save()
# Activity logging (see below)
issue_activity.delay(...)
return Response({"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK)
This validation prevents work-in-progress issues from being archived accidentally, enforcing project management hygiene.
Restoring Archived Issues
Restoration uses a DELETE request to the same archive endpoint (lines 80-99), which clears the archived_at field and logs the restoration activity:
def unarchive(self, request, slug, project_id, pk=None):
# Retrieve using archived manager to access soft-deleted records
issue = Issue.archived_objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
# Clear the archive flag
issue.archived_at = None
issue.save()
# Log restoration activity
issue_activity.delay(...)
return Response(status=status.HTTP_200_OK)
The system uses a separate manager (archived_objects) to access archived records, ensuring intentional access to soft-deleted data.
Bulk Archive Operations
For efficiency, Plane supports bulk archiving via POST /api/workspaces/{slug}/projects/{project_id}/bulk-archive/ (lines 105-143). This endpoint:
- Accepts a list of issue IDs in the request body
- Validates each issue's state (must be completed or cancelled)
- Sets
archived_atin memory for each valid issue - Performs a single
bulk_updateoperation to minimize database writes
# Conceptual flow from bulk archive implementation
issues_to_archive = []
for issue_id in issue_ids:
issue = Issue.issue_objects.get(pk=issue_id)
if issue.state.group in ["completed", "cancelled"]:
issue.archived_at = timezone.now().date()
issues_to_archive.append(issue)
# Log activity for each issue
issue_activity.delay(...)
# Single update operation
Issue.objects.bulk_update(issues_to_archive, ["archived_at"])
This approach reduces database round-trips when archiving multiple completed sprints or backlog items simultaneously.
Frontend Service Integration
The Plane web application abstracts these API endpoints through the IssueArchiveService in apps/web/core/services/issue/issue_archive.service.ts. This TypeScript service provides methods that wrap the REST calls:
import { IssueArchiveService } '@/services/issue/issue_archive.service';
// Archiving a single issue
const archiveIssue = async (workspace: string, project: string, issueId: string) => {
const service = new IssueArchiveService();
const result = await service.archiveIssue(workspace, project, issueId);
return result.archived_at;
};
// Restoring an archived issue
const restoreIssue = async (workspace: string, project: string, issueId: string) => {
const service = new IssueArchiveService();
await service.restoreIssue(workspace, project, issueId);
};
The service handles URL construction, authentication headers, and error parsing, providing a type-safe interface for the React frontend components.
Activity Logging and Audit Trail
Every archive and restore operation generates an audit entry via the issue_activity background task (apps/api/plane/bgtasks/issue_activities_task.py). The system records:
- The actor performing the action
- The timestamp (as epoch)
- The previous state (serialized issue data)
- The requested changes
- Automation flags
This creates a complete history of when issues were archived and restored, accessible through the activity feed for compliance and debugging purposes.
Summary
- Soft-delete mechanism: Plane uses a nullable
archived_atDateField rather than physical deletion, preserving all issue data and relationships. - Automatic filtering: The IssueManager custom manager automatically excludes archived issues from standard queries, ensuring archived data doesn't appear in active workflows.
- State enforcement: The API validates that issues are in completed or cancelled states before archiving, preventing accidental archival of active work.
- Bulk operations: The
bulk_updatemethod in the bulk archive endpoint optimizes performance for archiving multiple issues simultaneously. - Complete audit trail: The
issue_activitytask logs every archive and restore operation with full context and user attribution. - Frontend abstraction: The IssueArchiveService TypeScript class provides a clean interface for React components to interact with the archive API.
Frequently Asked Questions
How does Plane prevent archived issues from appearing in standard queries?
Plane implements a custom IssueManager in apps/api/plane/db/models/issue.py that automatically appends archived_at__isnull=True to every queryset. This means Issue.objects.all() and related queries only return active issues. To access archived issues, the codebase uses a separate manager or explicit filters that include non-null archived_at values.
Can any issue be archived, or are there restrictions?
The API enforces strict state validation before archiving. As implemented in apps/api/plane/app/views/issue/archive.py, the archive() method checks that issue.state.group is either "completed" or "cancelled". If an issue is in an active or backlog state, the API returns a 400 Bad Request error with the message "Can only archive completed or cancelled state group issue."
How does the bulk archive functionality handle partial failures?
The bulk archive endpoint iterates through the provided issue IDs individually, validating each issue's state before adding it to the archive list. Successfully validated issues are archived via a single bulk_update operation. While the provided code excerpt shows the happy path, production implementations typically validate that all issues are archivable before performing the bulk update, or return specific error messages for invalid issue IDs in the batch.
Is there a way to permanently delete archived issues?
The provided source code analysis focuses on soft-delete and restore functionality. While the archived_at field enables restoration, the system preserves the underlying database records indefinitely. For permanent deletion, Plane would likely implement separate administrative functions or data retention policies that operate outside the standard archive/restore workflow described in these files.
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 →