Issue Archival and Restore System in Plane: Technical Implementation Guide
Plane implements issue archival as a soft-delete mechanism using a nullable archived_at timestamp field on the Issue model, where archiving issues a POST request to set the timestamp and restoring sends a DELETE request to clear it.
The issue archival and restore system in Plane allows teams to declutter active boards without permanently deleting historical data. This feature spans the entire stack of the makeplane/plane repository, from the Django database models to the React frontend stores. Understanding this system reveals how Plane handles state transitions, optimistic UI updates, and filtered queries across workspaces and projects.
Database Schema and the archived_at Field
At the core of the archival system lies a single database field defined in the Django ORM. In apps/api/plane/db/models/issue.py, the Issue model includes an archived_at DateTime field that defaults to NULL for active issues.
When this field carries a timestamp, the issue is considered archived; when it is NULL, the issue is active. This design allows the database to index the field efficiently and enables fast filtering using simple SQL IS NULL or IS NOT NULL queries. The field is exposed through the API serializers located in apps/api/plane/serializers/issue.py, ensuring the frontend receives the ISO-formatted timestamp or null value with every issue payload.
Backend API Implementation
The backend handles archival logic through dedicated REST endpoints defined in apps/api/plane/app/views/workspace/issue.py.
Archive Endpoint Behavior
The system uses a consistent URL pattern for archival operations:
/api/workspaces/:workspaceSlug/projects/:projectId/issues/:issueId/archive/
-
POST requests to this endpoint trigger the archive action. The view sets
issue.archived_at = timezone.now()and persists the change, returning the timestamp to the caller. -
DELETE requests to the same endpoint trigger the restore (unarchive) action. The view clears the
archived_atfield by setting it toNULL, effectively restoring the issue to the active pool.
This convention treats the archive state as a sub-resource, making the API intuitive and idempotent.
Frontend Architecture
The frontend implements a layered architecture to handle user actions, API communication, and state management.
Service Layer (IssueArchiveService)
Located at apps/web/core/services/issue/issue_archive.service.ts, the IssueArchiveService abstracts the HTTP logic. It provides methods such as archiveIssue() and restoreIssue() that construct the proper URLs using workspace and project slugs, then dispatch POST or DELETE requests respectively. This service standardizes error handling and header configuration across the application.
State Management with MobX
Archived issue state is managed in apps/web/core/store/issue/archived/issue.store.ts. This MobX store maintains a local cache of archived issues and provides actions to move issues between active and archived collections. When an archive operation succeeds, the store immediately updates the local issue object with the archived_at timestamp, enabling optimistic UI updates before the server confirms the action.
UI Hooks and Components
User interactions flow through apps/web/core/hooks/use-issues-actions.tsx, which exposes convenient wrapper functions like archiveIssue and restoreIssue. These hooks combine the service calls with store updates and toast notifications.
The actual archive buttons appear in apps/web/core/components/issues/issue-layouts/quick-action-dropdowns/issue-detail.tsx, which renders the archive/unarchive options based on the current archived_at state. Clicking these triggers a confirmation modal before invoking the hook actions.
Step-by-Step Archival Workflow
The complete flow from user click to database update follows these steps:
-
User Initiates Action – The quick-action dropdown in the issue detail view calls the hook function, passing the workspace slug, project ID, and issue ID.
-
Service Request –
IssueArchiveService.archiveIssueconstructs the POST request to the backend endpoint. -
Backend Processing – The Django view (
IssueArchiveView) validates permissions, setsarchived_atto the current datetime, and saves the model. -
Response Handling – The API returns
{ archived_at: "<ISO-timestamp>" }, which the service passes back to the hook. -
State Update – The MobX store updates the issue's
archived_atproperty, causing React components to re-render and move the issue from the active list to the archived tab. -
Restoring – The reverse flow occurs for restoration: the hook calls
restoreIssue, the service issues a DELETE request, the backend clearsarchived_at, and the store removes the timestamp, returning the issue to active status.
Querying and Displaying Archived Issues
Plane provides a dedicated view for browsing archived issues at the route /archives/issues/. The implementation in apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx fetches issues using a filter parameter that requests only items where archived_at is not null.
Active issue lists throughout the application apply the inverse filter, ensuring archived items do not clutter boards, lists, or cycle views unless explicitly requested. This filtering happens at the API level, ensuring efficient database queries rather than client-side filtering of large datasets.
Summary
- Soft Delete Pattern: Plane uses an
archived_attimestamp rather than hard deletion, preserving all issue history and relationships. - RESTful API: The backend exposes
/archive/sub-resources that accept POST to archive and DELETE to restore. - Optimistic UI: MobX stores in
apps/web/core/store/issue/archived/issue.store.tsupdate immediately upon API success, providing instant feedback. - Clear Separation: The service layer (
IssueArchiveService), hook layer (use-issues-actions), and UI components remain decoupled, making the archival logic reusable across issues, epics, and other entities.
Frequently Asked Questions
How does Plane distinguish between active and archived issues?
Plane checks the archived_at field on the Issue model. When this field is NULL, the issue is considered active; when it contains a timestamp, the issue is archived. This boolean-like logic is implemented in the database queries and serializers located in apps/api/plane/db/models/issue.py and apps/api/plane/serializers/issue.py.
What happens to issue relationships when an issue is archived?
Archiving only modifies the archived_at timestamp and does not alter foreign key relationships or delete associated comments, attachments, or activity logs. The issue remains fully restorable with all relationships intact because the backend in apps/api/plane/app/views/workspace/issue.py only touches the archival timestamp field.
Can archived issues be queried through the standard API?
Yes, archived issues are accessible through the standard issue endpoints by including appropriate filter parameters. The dedicated archives page in apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx specifically requests issues where archived_at is not null, while standard views filter these out to show only active work.
Is the archival system available for entities other than issues?
Yes, the architecture supports archiving for epics, cycles, and other project entities. The IssueArchiveService in apps/web/core/services/issue/issue_archive.service.ts uses a serviceType parameter in its URL construction, allowing the same pattern to apply across different entity types with minimal code duplication.
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 →