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, providingcreate,remove,search, andupdatemethods 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 theinTimelineflag. - Permission Enforcement: Four distinct scopes (
PartnerCreate,PartnerRead,PartnerUpdate,PartnerDelete) defined inserver/src/enum.tsprotect these operations, with most enforced at the controller layer andPartnerUpdateenforced internally viarequireAccess. - Data Mapping: The
mapPartnermethod transforms database entities intoPartnerResponseDtoobjects, 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →