# How Plane Handles File Attachments and Storage: A Complete Architecture Guide

> Explore Plane's architecture for file attachments and storage. Learn how signed URLs stream uploads directly to object storage, separating payload handling from metadata management.

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

---

**Plane implements a signed-URL upload pattern that streams files directly from the client to external object storage (like AWS S3) after obtaining a pre-signed URL from the Django backend, separating binary payload handling from metadata management.**

Plane is an open-source project management platform that manages file attachments through a distributed architecture designed to minimize server load. The system keeps attachment metadata—names, sizes, and external storage identifiers—in PostgreSQL via Django models while delegating actual binary storage to scalable object stores. Understanding this architecture helps developers extend Plane's file handling capabilities or troubleshoot upload failures.

## Architecture Overview

Plane's attachment flow follows a six-stage client-server pipeline that eliminates server-side bottlenecks:

1. **UI Selection** – Users interact with React components ([`attachment-upload.tsx`](https://github.com/makeplane/plane/blob/main/attachment-upload.tsx)) to select files.
2. **Metadata Generation** – Utility functions create unique filenames and extract MIME types and sizes.
3. **Signed-URL Negotiation** – The client requests a pre-signed upload URL from the Django API (`/api/assets/v2/.../attachments/`).
4. **Direct Upload** – The browser streams the file directly to the external storage provider using the signed URL.
5. **Status Confirmation** – Once uploaded, the client notifies the API to mark the attachment as complete.
6. **Lifecycle Management** – Attachments are retrieved via `GET` requests and deleted via `DELETE` endpoints.

## Client-Side Implementation

### Metadata Preparation

Before any network request, Plane normalizes file metadata using utilities in [`packages/utils/src/attachment.ts`](https://github.com/makeplane/plane/blob/main/packages/utils/src/attachment.ts). The `generateFileName` function creates timestamped unique identifiers, while `getFileMetaDataForUpload` extracts the size and MIME type into a standardized payload.

```typescript
import { generateFileName, getFileMetaDataForUpload } from '@plane/utils';

const file = document.getElementById('file-input').files[0];
const metadata = {
  name: generateFileName(file.name),
  ...getFileMetaDataForUpload(file)
};
// Returns: { name: "20240115-abc123.jpg", size: 1024000, mime_type: "image/jpeg" }

```

### Upload Service Layer

The `IssueAttachmentService` class in [`apps/web/core/services/issue/issue_attachment.service.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/services/issue/issue_attachment.service.ts) orchestrates the three-phase upload process. It first posts metadata to obtain a signed URL, then delegates the binary transfer to `FileUploadService`, and finally patches the attachment status.

The key method `uploadIssueAttachment` handles the complete lifecycle:

```typescript
import { IssueAttachmentService } from '@/services/issue/issue_attachment.service';

const attachmentService = new IssueAttachmentService();

async function handleUpload(file: File, workspaceSlug: string, projectId: string, issueId: string) {
  try {
    const result = await attachmentService.uploadIssueAttachment(
      workspaceSlug,
      projectId,
      issueId,
      file,
      (progressEvent) => {
        const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
        console.log(`Progress: ${percent}%`);
      }
    );
    return result;
  } catch (error) {
    console.error('Upload failed:', error);
  }
}

```

## Backend API and Database Schema

### Django Models and Migrations

Attachment metadata resides in the **IssueAttachment** model, which was extended via migration [`0072_issueattachment_external_id_and_more.py`](https://github.com/makeplane/plane/blob/main/0072_issueattachment_external_id_and_more.py) in `apps/api/plane/db/migrations/`. This migration adds fields to link Plane records to external storage identifiers, storing the S3 key or equivalent without hosting the binary data in PostgreSQL.

The model maintains referential integrity to issues while keeping the actual payload in object storage, ensuring the database remains lightweight even with thousands of attachments.

### Signed URL Generation

The Django view at [`apps/api/plane/app/views/issue/attachment.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/views/issue/attachment.py) exposes the REST endpoints under `/api/assets/v2/`. When receiving a `POST` request with file metadata, the backend generates a pre-signed URL valid for a limited duration (typically S3 pre-signed URLs) and returns it along with the attachment record ID.

**Key endpoints in this flow:**
- `POST /api/assets/v2/workspaces/{slug}/projects/{id}/issues/{id}/attachments/` – Creates the attachment record and returns the signed URL.
- `PATCH /api/assets/v2/workspaces/{slug}/projects/{id}/issues/{id}/attachments/{id}/` – Updates the upload status after successful client-side transfer.
- `GET /api/assets/v2/workspaces/{slug}/projects/{id}/issues/{id}/attachments/` – Lists all attachments for an issue.
- `DELETE /api/assets/v2/workspaces/{slug}/projects/{id}/issues/{id}/attachments/{asset_id}/` – Removes the attachment reference.

## Step-by-Step Upload Flow

**Phase 1: Metadata Submission**
The client sends `{ name, size, mime_type }` to the backend. The Django API creates an `IssueAttachment` record in a "pending" state and generates a signed URL for the specific external storage path.

**Phase 2: Binary Streaming**
Using the `signedURLResponse.upload_data.url` returned by the API, `FileUploadService.uploadFile` streams the file bytes directly to the object store. This bypasses Plane's servers entirely, enabling large file support without consuming application bandwidth.

**Phase 3: Status Verification**
After the storage provider confirms the upload, the client calls `updateIssueAttachmentUploadStatus` via `PATCH` request. This marks the attachment as "uploaded" in the database, making it visible in the [`attachments-list.tsx`](https://github.com/makeplane/plane/blob/main/attachments-list.tsx) UI component for other users.

## Key Implementation Files

**Core Service Layer**
- [`apps/web/core/services/issue/issue_attachment.service.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/services/issue/issue_attachment.service.ts) – Implements `uploadIssueAttachment`, `getIssueAttachments`, and `deleteIssueAttachment` methods (lines 50-94).
- [`apps/web/services/api.service.ts`](https://github.com/makeplane/plane/blob/main/apps/web/services/api.service.ts) – Base `APIService` class providing authenticated HTTP client functionality.

**Utility Functions**
- [`packages/utils/src/attachment.ts`](https://github.com/makeplane/plane/blob/main/packages/utils/src/attachment.ts) – Contains `generateFileName` and `getFileMetaDataForUpload` for filename sanitization and metadata extraction.

**UI Components**
- [`apps/web/core/components/issues/attachment/attachment-upload.tsx`](https://github.com/makeplane/plane/blob/main/apps/web/core/components/issues/attachment/attachment-upload.tsx) – Drag-and-drop interface triggering the upload flow.
- [`apps/web/core/components/issues/attachment/attachments-list.tsx`](https://github.com/makeplane/plane/blob/main/apps/web/core/components/issues/attachment/attachments-list.tsx) – Renders uploaded attachments with delete actions.

**Backend Infrastructure**
- [`apps/api/plane/app/views/issue/attachment.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/views/issue/attachment.py) – Django REST API view handling signed URL generation.
- [`apps/api/plane/db/migrations/0072_issueattachment_external_id_and_more.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/migrations/0072_issueattachment_external_id_and_more.py) – Database schema linking attachments to external storage.

## Summary

- Plane uses a **signed-URL pattern** that separates metadata management from binary storage, keeping the Django API lightweight.
- The **IssueAttachment** model in PostgreSQL stores only references to external storage, while actual files reside in object stores like AWS S3.
- **Direct client-to-storage uploads** eliminate server bottlenecks and support progress tracking via XMLHttpRequest events.
- The **three-phase upload process** (metadata request → direct upload → status confirmation) ensures data integrity while maximizing scalability.
- All attachment operations route through `IssueAttachmentService` on the client and the `/api/assets/v2/` endpoints on the backend.

## Frequently Asked Questions

### How does Plane handle large file uploads without server timeouts?

Plane delegates the binary transfer to external object storage via pre-signed URLs. The client uploads directly to S3 (or equivalent), so the Django API only handles small JSON metadata requests, preventing server-side timeouts regardless of file size.

### Where is the actual file data stored in Plane's architecture?

The binary data lives in external object storage (typically AWS S3), while Plane's PostgreSQL database stores only metadata in the **IssueAttachment** model—including the filename, size, MIME type, and external storage identifier—enabling efficient querying without bloating the database.

### What happens if a user cancels an upload midway through?

Since uploads stream directly to object storage, cancelling the HTTP request to the signed URL simply leaves an orphaned object that may be cleaned up by storage-level lifecycle policies. The attachment record in Plane remains in a "pending" state until `updateIssueAttachmentUploadStatus` is called, and can be garbage collected by background jobs if left incomplete.

### Can I extend Plane's attachment system to support other storage providers?

Yes. The architecture abstracts storage specifics into the signed URL generation logic in [`apps/api/plane/app/views/issue/attachment.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/views/issue/attachment.py). Implementing a different provider requires modifying the backend to generate pre-signed URLs for your specific object store (Azure Blob, Google Cloud Storage, etc.) while keeping the client-side `IssueAttachmentService` interface unchanged.