# How AFFiNE Manages Blob Storage for Media and Attachments: Architecture and Implementation

> Discover how AFFINE's plugin-based blob storage abstracts cloud, IndexedDB, and SQLite, enabling unified media and attachment management across all environments. Learn its architecture and implementation.

- Repository: [Toeverything/AFFiNE](https://github.com/toeverything/AFFiNE)
- Tags: architecture
- Published: 2026-03-05

---

**AFFiNE implements a unified, plugin-based blob storage layer that abstracts cloud, IndexedDB, and SQLite backends behind a single `BlobStorage` interface, enabling seamless media handling across web, desktop, and offline environments.**

AFFiNE treats binary assets—images, videos, PDFs, and other attachments—as first-class citizens through a sophisticated storage architecture. This system, found in the `toeverything/AFFiNE` repository, decouples media management from specific storage technologies while enforcing quota limits, garbage collection, and multi-part upload strategies.

## The BlobStorage Interface and Base Architecture

At the core of AFFiNE's media management is a strict contract that all storage implementations must follow.

### Core Interface Definition

The `BlobStorage` interface is defined in [[`packages/common/nbstore/src/storage/blob.ts`](https://github.com/toeverything/AFFiNE/blob/main/packages/common/nbstore/src/storage/blob.ts)](https://github.com/toeverything/AFFiNE/blob/canary/packages/common/nbstore/src/storage/blob.ts). It mandates five essential operations:

- `get(key: string): Promise<BlobRecord | null>` – Retrieves a blob by its unique key.
- `set(record: BlobRecord): Promise<void>` – Stores a new blob or updates an existing one.
- `delete(key: string, permanently?: boolean): Promise<void>` – Removes a blob, with optional soft-deletion.
- `release(): Promise<void>` – Triggers garbage collection for soft-deleted entries.
- `list(): Promise<ListedBlobRecord[]>` – Enumerates all stored blobs with metadata.

### Base Class Implementation

The `BlobStorageBase` abstract class (lines 32–45 in the same file) provides the foundational structure. It forces concrete implementations to expose a `connection` property representing the underlying transport and an `isReadonly` flag to prevent accidental modifications in certain contexts.

## Cloud Blob Storage Implementation

When AFFiNE connects to a remote workspace, media assets flow through the cloud storage backend.

### HTTP Connection and API Endpoints

The `CloudBlobStorage` class in [[`packages/common/nbstore/src/impls/cloud/blob.ts`](https://github.com/toeverything/AFFiNE/blob/main/packages/common/nbstore/src/impls/cloud/blob.ts)](https://github.com/toeverything/AFFiNE/blob/canary/packages/common/nbstore/src/impls/cloud/blob.ts) manages server communication via an `HttpConnection` instantiated with the server's base URL. It maps blob keys to RESTful endpoints at `/api/workspaces/:id/blobs/:key`, handling manual redirect logic for mobile and Electron environments.

### Upload Strategies

AFFiNE optimizes uploads through three distinct strategies determined by server negotiation:

1. **GraphQL Upload** – Small files are posted directly via the `setBlobMutation` GraphQL endpoint.
2. **Presigned URL Upload** – For medium-sized assets, the client receives a time-limited signed URL and uploads directly to object storage (S3-compatible) via `uploadViaPresigned`.
3. **Multipart Upload** – Large files are chunked and uploaded in parallel using presigned URLs for each part, managed by `uploadViaMultipart`.

### Quota Management and Error Handling

Before any upload, `CloudBlobStorage` queries the workspace quota via `workspaceBlobQuotaQuery`. If the upload exceeds the allocated size, it throws an `OverSizeError`. Similarly, `OverCapacityError` triggers when the workspace exhausts its storage limit, providing user-friendly feedback through the UI.

## Local Storage Backends for Offline and Desktop

AFFiNE maintains functionality without an internet connection through embedded database implementations.

### IndexedDB Implementation

For browser-based usage, `IndexedDBBlobStorage` in [[`packages/common/nbstore/src/impls/idb/blob.ts`](https://github.com/toeverything/AFFiNE/blob/main/packages/common/nbstore/src/impls/idb/blob.ts)](https://github.com/toeverything/AFFiNE/blob/canary/packages/common/nbstore/src/impls/idb/blob.ts) persists data locally. It utilizes two object stores: `blobs` for metadata (key, mime type, size) and `blobData` for the raw binary content. Soft-deletion is tracked with a `deletedAt` timestamp, and the `release` method purges these records to reclaim space.

### SQLite Implementation

Desktop Electron builds leverage SQLite through two variants in [[`packages/common/nbstore/src/impls/sqlite/blob.ts`](https://github.com/toeverything/AFFiNE/blob/main/packages/common/nbstore/src/impls/sqlite/blob.ts)](https://github.com/toeverything/AFFiNE/blob/canary/packages/common/nbstore/src/impls/sqlite/blob.ts) and [`blob-sync.ts`](https://github.com/toeverything/AFFiNE/blob/main/blob-sync.ts). These provide the same `BlobStorage` API backed by a local SQL file, offering high-performance binary storage without browser storage limits.

## Frontend Integration and Utilities

### Buffer-to-Blob Conversion

When components receive raw binary data, the `buffer-to-blob` utility in [[`packages/frontend/core/src/modules/workspace-engine/utils/buffer-to-blob.ts`](https://github.com/toeverything/AFFiNE/blob/main/packages/frontend/core/src/modules/workspace-engine/utils/buffer-to-blob.ts)](https://github.com/toeverything/AFFiNE/blob/canary/packages/frontend/core/src/modules/workspace-engine/utils/buffer-to-blob.ts) converts `ArrayBuffer` instances into `File` objects. This bridges the gap between network responses and the storage layer's expected input format.

### Practical Usage Examples

The following patterns demonstrate common blob operations in AFFiNE applications:

```typescript
// Storing a new image from an ArrayBuffer
import { bufferToBlob } from '@affine/core/modules/workspace-engine/utils/buffer-to-blob';

async function uploadImage(workspace: Workspace, arrayBuffer: ArrayBuffer) {
  const blobRecord = {
    key: `${Date.now()}-upload.png`,
    data: new Uint8Array(arrayBuffer),
    mime: 'image/png',
  };
  
  // workspace.blobStorage resolves to CloudBlobStorage or IndexedDBBlobStorage
  await workspace.blobStorage.set(blobRecord);
}

// Retrieving a blob for display
async function fetchAttachment(storage: BlobStorage, key: string) {
  const record = await storage.get(key);
  if (!record) throw new Error('Attachment not found');
  
  return new Blob([record.data], { type: record.mime });
}

// Listing all workspace attachments
async function listStoredBlobs(storage: BlobStorage) {
  const blobs = await storage.list();
  return blobs.map(b => ({
    key: b.key,
    size: b.size,
    created: b.createdAt,
  }));
}

// Permanent deletion with garbage collection
await storage.delete('old-file.pdf', true);
await storage.release(); // Reclaims space from soft-deleted items

```

## Garbage Collection and Lifecycle Management

AFFiNE implements a two-phase deletion strategy to prevent accidental data loss while optimizing storage usage. When `delete` is called without the `permanently` flag, the system sets a `deletedAt` timestamp (observable in both `CloudBlobStorage` and `IndexedDBBlobStorage`). These soft-deleted records remain queryable but hidden from standard list operations.

The `release` method performs garbage collection by permanently purging all records marked for deletion. In cloud contexts, this triggers server-side cleanup routines; in local IndexedDB or SQLite implementations, it executes transactional deletes against the underlying stores. This pattern ensures that bulk cleanup operations can be deferred to optimal times, such as workspace closure or idle periods, without blocking user interactions.

## Summary

- AFFiNE abstracts all binary asset storage behind the `BlobStorage` interface defined in [`packages/common/nbstore/src/storage/blob.ts`](https://github.com/toeverything/AFFiNE/blob/main/packages/common/nbstore/src/storage/blob.ts), ensuring consistent API behavior across environments.
- **Cloud storage** (`CloudBlobStorage`) handles remote workspaces with intelligent upload strategies—GraphQL for small files, presigned URLs for medium files, and multipart uploads for large assets—while enforcing quotas via `workspaceBlobQuotaQuery`.
- **Local storage** implementations include `IndexedDBBlobStorage` for browsers and SQLite variants for Electron desktop builds, providing offline capability and high-performance local binary storage.
- The system supports **soft deletion** with timestamp-based marking and explicit `release` garbage collection, preventing accidental data loss while allowing space reclamation.
- Frontend utilities like `buffer-to-blob` bridge raw binary data with the storage layer, enabling seamless image and attachment workflows across web and desktop clients.

## Frequently Asked Questions

### How does AFFiNE handle large file uploads without blocking the UI?

AFFiNE employs a tiered upload strategy within `CloudBlobStorage` that adapts to file size. Small files upload via GraphQL mutations, while larger files use presigned URLs or multipart uploads where chunks transfer in parallel. This asynchronous approach prevents main-thread blocking, and quota checks occur before transmission to fail fast if limits are exceeded.

### What happens to my attachments when I switch from online to offline mode?

When connectivity drops, AFFiNE automatically falls back to local storage implementations. If you were using a cloud workspace, the system relies on cached data or switches to `IndexedDBBlobStorage` (in browsers) or SQLite (in desktop builds) for local workspaces. The `BlobStorage` abstraction ensures that `get`, `set`, and `list` operations function identically regardless of the underlying transport.

### How does AFFiNE prevent accidental deletion of important media files?

The storage layer implements soft-deletion by default. When `delete` is called without the `permanently` flag, AFFiNE sets a `deletedAt` timestamp rather than removing the data immediately. These items remain recoverable until `release` is invoked to perform garbage collection. This two-phase approach protects against accidental removals while allowing explicit permanent deletion when necessary.

### Where does AFFiNE store images and files in the desktop application?

Desktop builds utilize SQLite-based blob storage located in [`packages/common/nbstore/src/impls/sqlite/blob.ts`](https://github.com/toeverything/AFFiNE/blob/main/packages/common/nbstore/src/impls/sqlite/blob.ts). This implementation stores binary data directly in the local filesystem via SQLite, bypassing browser storage limits. The desktop client may also use [`blob-sync.ts`](https://github.com/toeverything/AFFiNE/blob/main/blob-sync.ts) for synchronization between local SQLite and remote cloud storage, ensuring consistency across devices.