All docs/image (proposed)

docs/architecture/image-processing-consolidation.md

Created: 2026-03-11 Status: Proposal Related: video-processor-app-architecture.md

Image Processing Consolidation — @repo/image

Problem Statement

Image processing conventions (variant sizes, formats, URL derivation, srcset construction) are scattered across 6+ locations in the monorepo. The same constants and patterns are duplicated independently, creating drift risk and making it hard to evolve the convention.

Current Scatter Map

What's Duplicated

ConcernLocationsRisk
Variant names (thumbnail, medium, full, xlarge, ultra)storage-types, process-image, image-preloader, themed-container, imageProcessingServiceAdd a variant → update 5 files
Size defaults (150, 800, 1920, 2560, 3840)storage-types Zod defaults, process-image inline, imageProcessingServiceSize change → update 3 files
Variant URL regeximage-preloader, themed-container (near-identical)Pattern drift
srcset builderimage-preloader (buildPreloadSrcset), themed-container (buildBackgroundSrcset)Divergent srcset logic
Variant selection by type (logo→medium, background→ultra)process-image.ts only (but convention undocumented)Implicit knowledge
Format + quality defaultsstorage-types Zod, imageProcessingServiceDrift between schema and implementation

Proposed Solution: @repo/image Package

A new packages/image/ package that becomes the single source of truth for image variant conventions — both producing and consuming sides.

Package Structure

packages/image/
├── src/
│   ├── constants.ts          # Variant names, sizes, formats, quality — THE source of truth
│   ├── variants.ts           # URL derivation, srcset builders, variant selection
│   ├── types.ts              # Re-export refined types (or keep in storage-types and import)
│   ├── server/
│   │   └── process.ts        # processUploadedImage (moved from @repo/video/client)
│   └── web/
│       ├── responsive-image.tsx   # Responsive <img> with convention-aware srcset
│       └── preload.ts             # Preload link helpers (extracted from image-preloader)
├── tsup.config.ts
└── package.json              # exports: ./constants, ./variants, ./server, ./web

What Moves Where

Current LocationMoves ToNotes
storage-types.ts size/format defaults@repo/image/constantsTypes stay in @repo/storage, but defaults imported from @repo/image
process-image.ts (variant selection, processUploadedImage)@repo/image/serverClient wrapper stays similar, constants imported
image-preloader.tsx regex + srcset builder@repo/image/variants + @repo/image/webShared utilities, component stays in app-video-flow but uses shared logic
themed-container.tsx variant derivation block@repo/image/variants~40 lines of utilities extracted; component imports them
imageProcessingService.ts size constantsImports from @repo/image/constantsService stays in video-processor (Sharp + Cloud Run), just uses shared constants

What Stays Put

  • imageProcessingService.ts stays in apps/video-processor — it's a Sharp/Cloud Run concern, not a shared package concern. It just imports constants.
  • storage-types.ts keeps the Zod schemas — they define the API contract. The default values reference @repo/image/constants.
  • Server actions stay in their apps — they're thin entrypoints. They call @repo/image/server instead of @repo/video/client.
  • progressive-image.tsx stays in design-system — it's a generic component, not image-variant-aware.
  • AnimatedThumbnail.tsx stays in app-library — video thumbnail display is a different concern.

Approach Options

Option A: Full Package Extraction (Recommended)

Create @repo/image with all shared constants, utilities, and the process-image client. Clean separation.

Pros: Single source of truth, clear ownership, easy to find and evolve. Cons: New package to maintain, migration touches several files. Effort: Medium (~15 files touched, mostly imports).

Option B: Consolidate Into @repo/storage

Since storage-types.ts already has the Zod schemas, add the variant utilities there too.

Pros: No new package, smaller change. Cons: @repo/storage becomes a grab-bag (it's about storage drivers — Redis, Blob, file). Image processing conventions are a different domain. The web/ exports (srcset builders, React components) don't belong in a storage package.

Option C: Consolidate Into @repo/video

process-image.ts already lives in @repo/video/client. Add constants and web utilities there.

Pros: No new package, process-image already there. Cons: @repo/video is about video processing (Whisper, speech-to-text). Image variant conventions aren't video. The web/ srcset utilities would be awkward in a video package.

Recommendation

Option A — it's the cleanest long-term. The convention surface is specific enough to warrant its own package but broad enough (5 apps consume it) to justify the extraction. It follows the monorepo's existing pattern of domain-focused packages.


Migration Strategy

The refactor should be incremental and non-breaking:

Phase 1: Create package with constants (no consumers yet)

  1. Create packages/image/ with constants.ts and variants.ts
  2. Export variant names, sizes, formats, quality defaults
  3. Export URL derivation utilities (deriveVariantUrl, isProcessedImage, buildSrcset)
  4. Build and verify

Phase 2: Migrate consumption side

  1. themed-container.tsx — replace inline utilities with @repo/image/variants imports
  2. image-preloader.tsx — replace inline regex/srcset with @repo/image/variants imports
  3. Server actions — point to @repo/image/server (or keep via @repo/video/client if it re-exports)

Phase 3: Migrate production side

  1. imageProcessingService.ts — import size/quality constants from @repo/image/constants
  2. storage-types.ts — import defaults from @repo/image/constants for Zod schemas
  3. process-image.ts — move to @repo/image/server, re-export from @repo/video/client for backward compat

Phase 4: Cleanup

  1. Remove duplicated constants and utilities from original locations
  2. Update any remaining imports
  3. Remove backward-compat re-exports once all consumers are migrated

Benefits

  1. Add a variant in one place — today adding xlarge/ultra touched 6 files across 4 packages. With @repo/image, it's one file + the Sharp resize loop.
  2. srcset logic shared — no more near-identical buildPreloadSrcset vs buildBackgroundSrcset.
  3. Variant selection documented — the logo→medium, background→ultra mapping lives in one explicit place.
  4. Type-safe variant referencesImageVariant type exported once, used everywhere.
  5. Easier testing — variant URL derivation can be unit-tested in isolation.

Dependency Graph (After)