Shared Link Service Architecture in Immich: How Public Links Are Generated and Managed

Immich implements a two-layer architecture where the web client handles URL construction and UI orchestration via shared-link.service.ts, while the NestJS server manages cryptographic keys, permissions, and persistence in server/src/services/shared-link.service.ts.

The immich-app/immich repository separates public sharing concerns across distinct client and server services. Understanding this shared link service architecture reveals how the platform balances user-friendly URL generation with robust security controls and Open Graph metadata support.

Architectural Overview: Client and Server Separation

Immich divides the public-share feature into two well-defined layers that communicate via the Immich SDK.

This separation ensures that sensitive operations like key generation and permission validation remain server-side, while the client focuses on presentation and user interaction.

Public URL Construction and Routing

The asUrl Helper Function

In web/src/lib/services/shared-link.service.ts, the asUrl function transforms a SharedLinkResponseDto into a fully qualified public URL. The implementation supports two routing patterns based on configuration:

export const asUrl = (sharedLink: SharedLinkResponseDto) => {
  const path = sharedLink.slug
    ? `s/${encodeURIComponent(sharedLink.slug)}`
    : `share/${encodeURIComponent(sharedLink.key)}`;
  return new URL(
    path,
    serverConfigManager.value.externalDomain || globalThis.location.origin,
  ).href;
};

Slug vs. Key Routing: When a user provides a human-readable slug, the URL uses the short format /s/:slug. Otherwise, the system falls back to the server-generated cryptographic key using the /share/:key pattern. The domain resolution prioritizes the externalDomain value from serverConfigManager, ensuring correct links in both on-premise and cloud deployments.

CRUD Operations Across the Stack

Client-Side SDK Wrappers

The client service maps UI actions to SDK methods, wrapping each call in error handling and event emission via eventManager. The operation flow follows this pattern:

Operation Client Helper SDK Method Server Method
Create handleCreateSharedLink createSharedLink SharedLinkService.create
Update handleUpdateSharedLink updateSharedLink SharedLinkService.update
Delete handleDeleteSharedLink removeSharedLink SharedLinkService.remove
Asset Management handleRemoveSharedLinkAssets removeSharedLinkAssets SharedLinkService.removeAssets / addAssets

Server-Side Business Logic

The SharedLinkService in server/src/services/shared-link.service.ts implements several critical security and data integrity mechanisms:

Permission Enforcement: The requireAccess helper (from src/services/base.service.ts) validates that the requesting user owns the target album or assets, checking for Permission.AlbumShare or Permission.AssetShare before allowing mutations.

Cryptographic Key Generation: For links without custom slugs, the server generates high-entropy identifiers using cryptoRepository.randomBytes(50), producing a secure random string stored as the key field.

Slug Uniqueness Constraints: Custom slugs are subject to a PostgreSQL unique constraint (shared_link_slug_uq). When violated, the server catches the error in handleError and converts it to a BadRequestException to prevent collisions.

Security Implementation and Metadata

Password Protection and Token Management

For password-protected links, the server generates access tokens using a SHA-256 hash of the link ID and password:

private asToken(sharedLink: { id: string; password: string }) {
  return this.cryptoRepository
    .hashSha256(`${sharedLink.id}-${sharedLink.password}`)
    .toString('base64');
}

The SharedLinkService.login method validates credentials and returns this token, which clients must include in subsequent authenticated requests to access the shared content.

Open Graph Metadata Generation

The getMetadataTags method constructs social-sharing previews without exposing internal API keys. It generates image URLs that embed the shared-link key as a query parameter (?key=...), allowing external services like Facebook or Twitter to fetch thumbnails while maintaining access control.

Complete Event Flow: From Creation to Sharing

The architecture handles a typical "Create Share" workflow through coordinated client-server interaction:

  1. User Initiation: The UI invokes handleCreateSharedLink with parameters like type: SharedLinkType.Album and an optional expiration date.
  2. SDK Transmission: The client calls createSharedLink, transmitting a SharedLinkCreateDto (defined in src/dtos/shared-link.dto.ts) to the server.
  3. Server Validation: SharedLinkService.create executes requireAccess checks, generates the key or validates the slug, and persists the record.
  4. Client Response Handling: Upon receiving the SharedLinkResponseDto, the client emits SharedLinkCreate via eventManager and triggers handleShowSharedLinkQrCode to display the scannable link.
  5. URL Finalization: The QR modal displays the URL produced by asUrl, completing a shareable, optionally password-protected link with customizable expiration.

Summary

  • Two-layer architecture separates UI orchestration (web/src/lib/services/shared-link.service.ts) from security-critical business logic (server/src/services/shared-link.service.ts).
  • Dual routing system supports human-readable slugs (/s/:slug) and cryptographic keys (/share/:key) via the asUrl helper.
  • Permission enforcement relies on requireAccess checks against Permission.AlbumShare and Permission.AssetShare before any mutation.
  • Cryptographic security includes 50-byte random keys for default links and SHA-256 hashed tokens for password-protected access.
  • Social metadata is generated server-side via getMetadataTags, enabling external previews without compromising API security.

Frequently Asked Questions

How does Immich handle custom slugs versus auto-generated keys?

Immich prioritizes user-defined slugs for readability. When present, the asUrl function routes to /s/:slug. If omitted, the server generates a 50-byte random key via cryptoRepository.randomBytes(50) and routes to /share/:key. The database enforces slug uniqueness through the shared_link_slug_uq constraint.

The server never transmits plaintext passwords. Instead, SharedLinkService generates a base64-encoded SHA-256 hash via the asToken method, combining the link ID and password. Clients receive this token only after successful authentication through SharedLinkService.login, and must present it for subsequent asset requests.

Yes. The SharedLinkCreateDto and SharedLinkEditDto (defined in src/dtos/shared-link.dto.ts) include an optional expiresAt Date field. The server evaluates this during access attempts, invalidating expired links automatically without requiring manual cleanup.

How does the architecture support on-premise deployments?

The asUrl function dynamically resolves the public domain by checking serverConfigManager.value.externalDomain first, falling back to globalThis.location.origin only when undefined. This ensures generated links reference the correct external domain configured by the administrator, regardless of internal network topology.

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 →