# Immich Archive and Trash Service Implementations: A Deep Dive into Asset Visibility States

> Explore Immich's archive and trash features. Understand how visibility states and background jobs manage asset deletion and retrieval with AssetService, TimelineService, and TrashService.

- Repository: [Immich/immich](https://github.com/immich-app/immich)
- Tags: deep-dive
- Published: 2026-02-27

---

**Immich manages archive and trash through distinct visibility states handled by AssetService, TimelineService, and TrashService, using soft-delete patterns with background job queues for permanent removal.**

The immich-app/immich repository implements asset lifecycle management through sophisticated service-layer abstractions that separate soft-deletion (trash) from visibility filtering (archive). Understanding these service-level implementations for the archive and trash features in Immich reveals how the platform balances data integrity with user experience through permission-gated state transitions and asynchronous cleanup workflows.

## Archive Feature Implementation

### Asset Visibility Enum and Database Layer

Immich defines the archive state through the **AssetVisibility** enum in [`server/src/enum.ts`](https://github.com/immich-app/immich/blob/main/server/src/enum.ts). At line 129, the `Archive` value marks assets as archived in the database `visibility` column, creating a logical separation from active timeline content without deleting underlying files.

The **AssetService.updateAll()** method in [`server/src/services/asset.service.ts`](https://github.com/immich-app/immich/blob/main/server/src/services/asset.service.ts) (lines 45-53) handles the state transition. This service method receives an array of asset IDs and the target visibility value, performing a bulk update that immediately removes archived content from default timeline views while preserving the data for specialized archive queries.

### Permission Control and Timeline Access

Access to archived content is strictly controlled through **Permission.ArchiveRead**. The **TimelineService** in [`server/src/services/timeline.service.ts`](https://github.com/immich-app/immich/blob/main/server/src/services/timeline.service.ts) enforces this permission at lines 60-62 before returning any archived buckets. 

When fetching archived assets, `TimelineService.getTimeBuckets()` (lines 70-78) filters results based on the caller's permissions. Users without `ArchiveRead` permission receive timelines that exclude archived assets entirely, while authorized users can query specific archive views by passing `visibility: AssetVisibility.Archive` in their request parameters.

For administrative purposes, **AssetService.getStatistics()** (lines 54-61) supports visibility-based aggregation, allowing queries that return counts of archived assets per user to support quota management and storage analytics.

## Trash Feature Implementation

### Soft-Delete Status and AssetService.deleteAll()

Unlike archive, trash represents a deletion workflow using **AssetStatus.Trashed** (defined in [`server/src/enum.ts`](https://github.com/immich-app/immich/blob/main/server/src/enum.ts)). The **AssetService.deleteAll()** method at lines 85-97 in [`server/src/services/asset.service.ts`](https://github.com/immich-app/immich/blob/main/server/src/services/asset.service.ts) implements the soft-delete logic.

When `force` is set to `false`, the method updates the `deletedAt` timestamp and sets `status = AssetStatus.Trashed`, triggering the `AssetTrashAll` event. If `force` is `true`, the system bypasses trash and marks assets as `Deleted` immediately. This design allows users to recover accidentally deleted content while maintaining an audit trail through the `deletedAt` metadata.

### TrashService for Restore and Empty Operations

The **TrashService** in [`server/src/services/trash.service.ts`](https://github.com/immich-app/immich/blob/main/server/src/services/trash.service.ts) orchestrates trash lifecycle operations beyond the initial soft-delete. 

For restoration, `TrashService.restoreAssets()` (lines 12-22) validates the caller possesses `Permission.AssetDelete`, invokes `trashRepository.restoreAll(ids)` to clear the `deletedAt` and `status` fields, and emits the `AssetRestoreAll` event for downstream synchronization.

Emptying the trash initiates a background workflow through `TrashService.empty()` (lines 35-65). Rather than performing synchronous deletion, this service queues the `JobName.AssetEmptyTrash` background task. The `handleEmptyTrash` implementation streams all soft-deleted asset IDs via `trashRepository.getDeletedIds()` and creates individual `AssetDelete` jobs for each asset, preventing request timeouts during bulk permanent deletion.

### Background Job Processing for Permanent Deletion

Permanent removal occurs asynchronously through **AssetService.handleAssetDeletion()** (lines 22-34). This background worker processes the queued `AssetDelete` jobs, performing the actual file system removal of original images and thumbnails, updating database records to `AssetStatus.Deleted`, and emitting the final `AssetDelete` event.

The system also maintains automatic cleanup through `TrashService.onAssetsDelete()` (lines 43-46), an event listener that triggers `handleEmptyTrash` whenever bulk-delete events occur, ensuring that forced deletions propagate correctly through the trash workflow.

## Practical Code Examples

### Archiving Assets via AssetService

```typescript
// server/src/controllers/asset.controller.ts or equivalent
await this.assetService.updateAll(auth, {
  ids: ['asset-123', 'asset-456'],
  visibility: AssetVisibility.Archive,  // From server/src/enum.ts
});

```

This updates the `visibility` column and removes assets from default timeline queries.

### Querying Archived Content with TimelineService

```typescript
// Fetching archived timeline buckets
const buckets = await this.timelineService.getTimeBuckets(auth, {
  userId: auth.user.id,
  visibility: AssetVisibility.Archive,
});

```

The service validates `Permission.ArchiveRead` before returning results.

### Moving Assets to Trash

```typescript
// Soft-delete (move to trash)
await this.assetService.deleteAll(auth, {
  ids: ['asset-789'],
  force: false,  // false triggers soft-delete, sets AssetStatus.Trashed
});

```

Sets `deletedAt` and fires the `AssetTrashAll` event.

### Restoring from Trash

```typescript
// Restore to active timeline
await this.trashService.restoreAssets(auth, { 
  ids: ['asset-789'] 
});

```

Emits `AssetRestoreAll` and clears deletion timestamps.

### Emptying Trash via Background Jobs

```typescript
// Initiates async permanent deletion
await this.trashService.empty(auth);

// Background execution flow:
// 1. handleEmptyTrash streams IDs from trashRepository.getDeletedIds()
// 2. Queues JobName.AssetDelete for each asset
// 3. AssetService.handleAssetDeletion() removes files and DB rows

```

## Summary

- **Archive and trash represent distinct visibility states**: Archive uses `AssetVisibility.Archive` for read-only filtering, while trash uses `AssetStatus.Trashed` for soft-delete workflows.
- **AssetService** handles state mutations: `updateAll()` manages archive visibility, while `deleteAll()` initiates soft-delete or permanent removal.
- **TimelineService** enforces archive permissions: Requires `Permission.ArchiveRead` to include archived assets in timeline queries.
- **TrashService** orchestrates lifecycle operations: Manages restore functionality and queues background jobs for permanent deletion via `handleEmptyTrash`.
- **Background workers** handle physical deletion: `AssetService.handleAssetDeletion()` performs actual file system cleanup asynchronously to prevent API blocking.

## Frequently Asked Questions

### How does Immich distinguish between archived and trashed assets?

Archived assets retain `AssetStatus.Active` but use `visibility = AssetVisibility.Archive`, keeping them in the database but filtered from standard views. Trashed assets use `status = AssetStatus.Trashed` with a populated `deletedAt` timestamp, representing a soft-delete state that precedes physical removal. The distinction allows archived content to remain accessible indefinitely while trashed content is scheduled for permanent purging.

### What permissions are required to access archived photos in Immich?

Users require **Permission.ArchiveRead** to view archived content. The **TimelineService** enforces this in [`server/src/services/timeline.service.ts`](https://github.com/immich-app/immich/blob/main/server/src/services/timeline.service.ts) at lines 60-62, filtering archived assets from responses when the permission is absent. All trash operations (trash, restore, empty) require **Permission.AssetDelete** instead, which controls modification rights rather than visibility access.

### How does Immich handle permanent deletion when emptying the trash?

Emptying the trash initiates an asynchronous workflow through **TrashService.empty()** in [`server/src/services/trash.service.ts`](https://github.com/immich-app/immich/blob/main/server/src/services/trash.service.ts). The service queues a background job that streams all trashed asset IDs and creates individual deletion jobs. **AssetService.handleAssetDeletion()** then processes these jobs to remove files from storage and update database records, preventing long-running synchronous operations that could timeout during bulk deletion.

### Can archived assets be restored to the main timeline?

Yes. Since archived assets are never marked for deletion, restoring them requires only changing their visibility status back to `AssetVisibility.Timeline` or null through **AssetService.updateAll()**. This operation requires `Permission.AssetUpdate` (enforced by the access control layer) and immediately returns the assets to standard timeline queries without the need for trash-specific restoration workflows.