How AFFiNE Manages Blob Storage for Media and Attachments: Architecture and Implementation
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/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/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:
- GraphQL Upload – Small files are posted directly via the
setBlobMutationGraphQL endpoint. - 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. - 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/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/canary/packages/common/nbstore/src/impls/sqlite/blob.ts) and 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/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:
// 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
BlobStorageinterface defined inpackages/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 viaworkspaceBlobQuotaQuery. - Local storage implementations include
IndexedDBBlobStoragefor 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
releasegarbage collection, preventing accidental data loss while allowing space reclamation. - Frontend utilities like
buffer-to-blobbridge 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. 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 for synchronization between local SQLite and remote cloud storage, ensuring consistency across devices.
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 →