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

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. 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 (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 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). The AssetService.deleteAll() method at lines 85-97 in 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 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

// 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

// 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

// 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

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

Emits AssetRestoreAll and clears deletion timestamps.

Emptying Trash via Background Jobs

// 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 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. 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →