How Partner Sharing Relationships Are Managed in Immich: A Deep Dive into `partner.service.ts`

Partner sharing relationships in Immich are managed through the PartnerService class which provides CRUD operations for creating, reading, updating, and deleting partner connections, enforced by four distinct permission scopes defined in the Permission enum.

The immich-app/immich repository implements partner sharing through a dedicated service layer that handles the business logic for granting library access between users. Understanding how partner.service.ts manages these relationships and their associated permission scopes is essential for developers extending Immich's access control features or debugging sharing issues.

Core CRUD Operations in PartnerService

The PartnerService class in server/src/services/partner.service.ts exposes four primary methods that manipulate the Partner entity. Each operation enforces specific permission checks either at the controller layer or within the service itself.

Creating Partner Relationships

The create method establishes a new directional relationship where one user (the caller) shares their library with another user (the target).

// server/src/services/partner.service.ts lines 12-20
async create(auth: AuthDto, dto: PartnerCreateDto) {
  const partnerIds: PartnerIds = { sharedById: auth.user.id, sharedWithId: dto.sharedWithId };
  // Verify relationship doesn't already exist
  const existing = await this.partnerRepository.get(partnerIds);
  if (existing) {
    throw new BadRequestException('Partner already exists');
  }
  // Persist new partner relationship
  const partner = await this.partnerRepository.create(partnerIds);
  return this.mapPartner(partner, PartnerDirection.SharedBy);
}

This operation requires Permission.PartnerCreate, which the controller checks before invoking the service.

Removing Partner Connections

The remove method deletes an existing partner relationship using the same PartnerIds composite key structure.

// server/src/services/partner.service.ts lines 22-30
async remove(auth: AuthDto, id: string) {
  const partnerIds: PartnerIds = { sharedById: auth.user.id, sharedWithId: id };
  const partner = await this.partnerRepository.get(partnerIds);
  if (!partner) {
    throw new BadRequestException('Partner not found');
  }
  await this.partnerRepository.remove(partnerIds);
}

The controller enforces Permission.PartnerDelete for this operation.

Searching and Listing Partners

The search method retrieves all partner relationships for the authenticated user, supporting directional filtering via the PartnerDirection enum.

// server/src/services/partner.service.ts lines 32-48
async search(auth: AuthDto, dto: PartnerSearchDto) {
  const { direction } = dto;
  const partners = await this.partnerRepository.getAll(auth.user.id);
  return partners
    .filter((partner) => {
      const filterBy = direction === PartnerDirection.SharedBy ? partner.sharedById : partner.sharedWithId;
      return filterBy === auth.user.id;
    })
    .filter((partner) => !partner.sharedBy.isDeleted && !partner.sharedWith.isDeleted)
    .map((partner) => this.mapPartner(partner, direction));
}

This requires Permission.PartnerRead and automatically filters out relationships involving soft-deleted users.

Updating Partner Settings

The update method modifies partner-specific settings, currently limited to the inTimeline boolean flag that controls whether the partner's assets appear in the user's timeline.

// server/src/services/partner.service.ts lines 50-58
async update(auth: AuthDto, id: string, dto: PartnerUpdateDto) {
  await this.requireAccess({ permission: Permission.PartnerUpdate, ids: [id] });
  const partnerIds: PartnerIds = { sharedById: id, sharedWithId: auth.user.id };
  const partner = await this.partnerRepository.update(partnerIds, { inTimeline: dto.inTimeline });
  return this.mapPartner(partner, PartnerDirection.SharedWith);
}

Unlike other methods, update enforces Permission.PartnerUpdate internally via the requireAccess method inherited from BaseService (server/src/services/base.service.ts).

Permission Scopes for Partner Sharing

The four permission scopes governing partner relationships are defined in server/src/enum.ts (lines 82-86) as part of the central Permission enum:

Enum Member Scope String Description
PartnerCreate partner.create Grants ability to initiate new partner sharing relationships
PartnerRead partner.read Grants ability to view existing partner relationships
PartnerUpdate partner.update Grants ability to modify partner settings like inTimeline
PartnerDelete partner.delete Grants ability to terminate partner relationships

These scopes are enforced at the API controller layer for create, remove, and search operations, while the update method performs its permission check internally using requireAccess from the base service class.

Data Mapping and Response DTOs

The private mapPartner method (lines 50-57 in partner.service.ts) transforms database entities into API responses:

private mapPartner(partner: PartnerEntity, direction: PartnerDirection): PartnerResponseDto {
  const isSharedBy = direction === PartnerDirection.SharedBy;
  const user = isSharedBy ? partner.sharedWith : partner.sharedBy;
  return {
    ...mapUser(user),
    inTimeline: partner.inTimeline,
  };
}

This method determines which user profile to expose based on the query direction, ensuring the API returns the partner's information (not the caller's) while appending the relationship-specific inTimeline flag.

End-to-End Implementation Example

The following example demonstrates the complete lifecycle of a partner sharing relationship as implemented in partner.service.ts:

// 1. Alice creates a partner relationship with Bob
await partnerService.create(
  { user: { id: 'alice-id' } },          // AuthDto
  { sharedWithId: 'bob-id' }             // PartnerCreateDto
);
// Controller checks Permission.PartnerCreate
// Persists: { sharedById: 'alice-id', sharedWithId: 'bob-id' }

// 2. Bob searches for partners shared with him
await partnerService.search(
  { user: { id: 'bob-id' } },
  { direction: PartnerDirection.SharedWith }
);
// Controller checks Permission.PartnerRead
// Returns PartnerResponseDto for Alice

// 3. Bob hides Alice's assets from his timeline
await partnerService.update(
  { user: { id: 'bob-id' } },
  'alice-id',                             // sharedById (owner)
  { inTimeline: false }                   // PartnerUpdateDto
);
// Service checks Permission.PartnerUpdate via requireAccess
// Updates inTimeline flag on the Partner row

Summary

  • PartnerService Location: The core business logic resides in server/src/services/partner.service.ts, providing create, remove, search, and update methods for managing partner relationships.
  • CRUD Operations: The service handles creation of directional relationships, deletion by composite key (sharedById + sharedWithId), filtered searching by direction, and updates to the inTimeline flag.
  • Permission Enforcement: Four distinct scopes (PartnerCreate, PartnerRead, PartnerUpdate, PartnerDelete) defined in server/src/enum.ts protect these operations, with most enforced at the controller layer and PartnerUpdate enforced internally via requireAccess.
  • Data Mapping: The mapPartner method transforms database entities into PartnerResponseDto objects, ensuring the API returns the correct user profile based on query direction while including relationship-specific metadata.

Frequently Asked Questions

What permission is required to create a partner sharing relationship in Immich?

Creating a partner relationship requires the PartnerCreate permission (scope string partner.create), which is defined in server/src/enum.ts and enforced at the controller layer before PartnerService.create is invoked. The authenticated user becomes the sharedById while the target user becomes the sharedWithId.

How does PartnerService determine which user is the "other" party in a relationship?

The mapPartner method in server/src/services/partner.service.ts determines the other party by examining the direction parameter. When the direction is SharedBy, it returns partner.sharedWith; when SharedWith, it returns partner.sharedBy. This ensures the API always returns the partner's user profile rather than the caller's.

Can a shared-with user modify the inTimeline setting for a partner relationship?

Yes, the shared-with user can update the inTimeline flag via the update method, which modifies whether the partner's assets appear in their timeline. This operation requires Permission.PartnerUpdate, enforced internally through the requireAccess method inherited from BaseService, and uses a PartnerIds key where sharedById is the partner owner and sharedWithId is the caller.

Where are the permission scopes for partner sharing defined in the codebase?

The four permission scopes for partner sharing—PartnerCreate, PartnerRead, PartnerUpdate, and PartnerDelete—are defined as enum members in server/src/enum.ts (lines 82-86). These map to scope strings (partner.create, partner.read, partner.update, partner.delete) used throughout the API layer and service layer to enforce access control.

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 →