Skip to content

Analysis: Backend Services for Item Image Upload

Author: Claude Code for jmpicnic | Date: 2026-03-31 | Status: Draft

Analysis: Backend Services for Item Image Upload

Section titled “Analysis: Backend Services for Item Image Upload”

This project is a Modification — it adds new image upload capabilities to existing common-module and operations repositories while refactoring the existing CSV S3 access layer to extract shared capabilities. The analysis compares the backend specification against the current implementation to identify gaps, extractable patterns, and modification points.

The current implementation has no image-specific code. All image upload capabilities are new. However, the existing CsvS3BucketDirectAccess class contains S3 presigning, HEAD verification, and metadata validation logic that can be generalized into a shared S3AssetService. The refactoring scope is bounded: CSV-specific behavior (GET with decompression, row/batch flow parsing) remains in CsvS3BucketDirectAccess; only the S3 interaction primitives are extracted.


Specification vs. Implementation Comparison

Section titled “Specification vs. Implementation Comparison”

Category 1: Significant Gaps (Features Not Implemented)

Section titled “Category 1: Significant Gaps (Features Not Implemented)”

Specification (BE-FR-001, TD-08): Generate presigned POST forms with policy conditions enforcing key, Content-Type, content-length-range, tenant-id, author, arda-key, and SSE. Duration: 15 minutes.

Current state: CsvS3BucketDirectAccess generates presigned PUT URLs via S3Presigner.presignPutObject() (CsvS3ObjectDirectService.kt:169-185). Metadata is passed as signed headers on the PUT request. There is no presigned POST capability anywhere in the codebase.

Gap: Presigned POST requires the AWS SDK’s PresignedPostRequest / S3Presigner.presignPost() API (or equivalent createPresignedPost()), which constructs a policy document with conditions. This is fundamentally different from presigned PUT — it’s not a parameter change but a different AWS SDK operation with a different request/response shape.

Impact: High. This is the core new capability. Cannot be achieved by modifying existing code — requires new S3AssetService class.

Specification (BE-FR-003, BE-FR-007, TD-05, TD-15): Construct CDN URLs from object keys. Validate that submitted imageUrl values match the expected CDN host and key pattern. Reject non-CDN URLs and cross-tenant URLs. Per TD-15, validation is skipped when the imageUrl is unchanged from the persisted value (backward-compatibility for pre-existing non-CDN URLs). The upstream backend specification BE-FR-003 has been updated with this note (TD-21).

Implementation hint (TD-20): The tenant ID is available from ApplicationContext (Kotlin coroutine context), following the ScopedUniverse pattern in common-module. CdnUrlResolver can access the requesting tenant without explicit parameter threading.

Current state: No CDN URL logic exists. The imageUrl field is validated only as a parseable URL in ItemInput.toItem() (Model.kt:43-87) using URI(imageUrl).toURL(). No host pattern, key format, or tenant prefix validation.

Gap: Entirely new. Requires CdnUrlResolver class in common-module.

Impact: High. Required for both presigned credential response (BE-FR-007) and entity persistence validation (BE-FR-003).

Specification (BE-FR-002, TD-16): Construct S3 object keys in format <tenantId>/images/<uuid>.<ext>. UUID generated server-side. Extension derived from content type.

Implementation hint (TD-20): The tenant ID is available from ApplicationContext (Kotlin coroutine context).

Current state: CsvS3BucketDirectAccess constructs keys via fileKey(key) = "${keyNamespace}/${key.trimEnd('.')}.${fileSuffix}" (CsvS3ObjectDirectService.kt:159). This is CSV-specific — namespace-prefixed with compression suffix. No tenant-scoped key generation exists.

Gap: Entirely new. Requires AssetKeyGenerator class in common-module.

Impact: Medium. Foundational for key construction.

Design (TD-19): AssetKeyGenerator is a class with a configurable feature namespace (e.g., "images") validated at construction time as a URL path segment compatible format. Fail-fast if invalid. The configurable namespace supports future asset types (e.g., "documents") and pushes the design toward a class rather than a bare function.

Specification (BE-FR-001, TD-13): POST /v1/item/<itemEId>/image-upload-url returns presigned POST credentials alongside CDN URL.

Current state: No image upload endpoint exists. The item module has CRUD endpoints (ItemEndpoint.kt) for entity management, CSV upload routes, print routes, and supply routes — but no presigned credential endpoint.

Gap: Entirely new. Requires ImageUploadEndpoint in operations. Must be registered in ItemEndpoint.buildRoot() routing.

Impact: High. Primary API surface for the feature.

1.5 Image URL Validation Before Entity Persist

Section titled “1.5 Image URL Validation Before Entity Persist”

Specification (BE-FR-003, BE-FR-004, TD-15): Before persisting imageUrl, validate CDN pattern match and tenant prefix, then HEAD the object to verify existence and metadata. Skip validation when URL is unchanged (grandfathering).

Current state: ItemService.create() and update() call inputTranslatorItemInput.toItem() which only validates URL parseability. The service layer (ItemService.kt:48-180) has no image-specific validation step. The imageUrl flows through enrichPayload() untouched (enrichment only applies to supply references).

Gap: Requires modification of ItemService (or a pre-persist hook) to add CDN validation + HEAD verification. Also requires access to the persisted entity’s current imageUrl to implement TD-15 grandfathering.

Impact: High. Core security and data integrity requirement.

Implementation hints: See TD-15 (grandfathering) and TD-20 (ApplicationContext for tenant ID).


Specification: Presigned POST with policy document containing 7 conditions.

Current: Presigned PUT with signed headers for metadata. PUT does not support content-length-range enforcement or policy-based Content-Type validation — the client can upload any content type or size.

Difference: POST provides server-side enforcement that PUT cannot. This is a deliberate design decision (TD-08), not an implementation shortcoming.

Design (TD-17): S3AssetService must support both presigned PUT and presigned POST. Callers choose the appropriate method. CSV upload calls presignedPutUrl(); image upload calls createPresignedPost(). Role assumption is managed internally: default credentials for PUT, assumed presigning role for POST. This isolates future IAM role consolidation to a single class.

Specification: Metadata (tenant-id, author, arda-key, SSE) baked into POST policy conditions. Client includes form fields; S3 validates against policy.

Current: Metadata passed as x-amz-meta-* signed headers on PUT request (CsvS3ObjectDirectService.kt:175-177). Client must include exact headers.

Difference: Both enforce metadata, but POST policy is stricter — the policy is a signed JSON document that cannot be tampered with. The extraction should preserve the metadata validation pattern while supporting both enforcement mechanisms.

Specification: AssetKeyGenerator constructs keys server-side. Client never constructs keys.

Current: CsvS3BucketDirectAccess constructs keys via fileKey(), but the caller provides the base key and the class applies namespace + suffix. The namespace (cfg.name = module name) and suffix (.csv.gz) are CSV-specific.

Difference: Image keys are tenant-scoped (<tenantId>/images/...) while CSV keys are namespace-scoped (<moduleName>/...). The key construction logic is fundamentally different — shared only at the S3 API level, not at the key format level.


Category 3: Extractable Common Capabilities

Section titled “Category 3: Extractable Common Capabilities”

These capabilities currently exist in CsvS3BucketDirectAccess and can be extracted into S3AssetService:

#CapabilityCurrent LocationExtraction Target
3.1S3Presigner creation (region + credentials)CsvS3ObjectDirectService.kt:150-153S3AssetService constructor
3.2S3AsyncClient creation with configCsvS3ObjectDirectService.kt:119-137S3AssetService factory or shared builder
3.3HEAD object requestCsvS3ObjectDirectService.kt:212-232S3AssetService.headObject()
3.4Metadata validation (required + matching)CsvS3ObjectDirectService.kt:187-210S3AssetService.validateMetadata()
3.5Error wrapping (AppError.ExternalService)CsvS3ObjectDirectService.kt:224-231Shared error handling in S3AssetService
3.6Signature duration configurationCsvS3ObjectDirectService.kt:111S3AssetService constructor parameter
3.7Presigned PUT URL generationCsvS3ObjectDirectService.kt:169-185S3AssetService.presignedPutUrl() (TD-17)

Not extractable (CSV-specific, remains in CsvS3BucketDirectAccess):

#CapabilityReason
N.1GET object with decompressionCSV-specific (GZIP/BZIP2 decompression)
N.2CSV row/batch flow parsingCSV-specific (kotlin-csv, header normalization)
N.3Compression type handlingCSV-specific (CompressionType enum, file suffix)
N.4File key construction (${namespace}/${key}.${suffix})CSV-specific key format

Design decision resolved (TD-18): CsvS3BucketDirectAccess delegates all S3 primitives (presigned PUT, HEAD, metadata validation) to S3AssetService. This promotes cohesive abstractions — S3 interaction primitives belong in one class. CsvS3BucketDirectAccess retains only CSV-specific behavior: GET with decompression, row/batch flow parsing, compression handling, and CSV key construction.


Category 4: Local (operations) Infrastructure Gaps

Section titled “Category 4: Local (operations) Infrastructure Gaps”

4.1 CloudFormation — Missing Image Infrastructure Imports

Section titled “4.1 CloudFormation — Missing Image Infrastructure Imports”

Current (pre-install.cfn.yml): Imports only CSV upload infrastructure:

  • ${Infrastructure}-${Purpose}-API-UploadBucketArn (line 96)
  • ${Infrastructure}-${Purpose}-API-UploadBucketPresignRoleArn (line 123)

Required: Additional imports for image infrastructure:

  • ${Infrastructure}-${Purpose}-API-ImageAssetBucketArn
  • ${Infrastructure}-${Purpose}-API-ImageAssetBucketName
  • ${Infrastructure}-${Purpose}-API-ImagePresignRoleArn
  • ${Infrastructure}-${Purpose}-API-ImageCdnDomain

Gap: Four new Fn::ImportValue references and two new IAM policies (S3 access + AssumeRole for presigning) following the existing pattern.

4.2 Helm Configuration — Missing Image Values

Section titled “4.2 Helm Configuration — Missing Image Values”

Current (configmap.yaml): Wires only uploadBucketArn:

system.reference.item.extras.uploadBucketArn={{ .Values.system.reference.item.extras.uploadBucketArn | required }}

Required: Additional properties for image infrastructure:

  • system.reference.item.extras.imageAssetBucketArn
  • system.reference.item.extras.imageAssetBucketName
  • system.reference.item.extras.imagePresignRoleArn
  • system.reference.item.extras.imageCdnDomain

4.3 Module Configuration — Missing Image Service Wiring

Section titled “4.3 Module Configuration — Missing Image Service Wiring”

Current (Module.kt:89-92, 133): Creates CsvS3BucketDirectAccess from uploadBucketArn config. No image service wiring.

Required: Load image infrastructure config from extras, create S3AssetService, AssetKeyGenerator, CdnUrlResolver, wire into ItemService and new ImageUploadEndpoint.


Specification RequirementStatusCategoryNotes
BE-FR-001: Presigned POST generationGap1.1New capability — no POST support exists
BE-FR-002: Tenant-scoped key constructionGap1.3New AssetKeyGenerator class
BE-FR-003: CDN URL validationGap1.2New CdnUrlResolver class
BE-FR-004: HEAD verification before persistGap1.5New validation step in ItemService
BE-FR-005: Null imageUrl clears fieldOKAlready works — null propagates through
BE-FR-006: Bitemporal version historyOKUniverse handles this automatically
BE-FR-007: Return CDN URL with presigned formGap1.4New ImageUploadEndpoint response
BE-NFR-001: Presigning < 1s P95GapUntested — depends on implementation
BE-NFR-002: IAM scoped to bucket+prefixGap4.1CloudFormation policy needed
BE-NFR-003: Idempotent endpointGap1.4Each call generates new UUID/credentials
TD-15: Grandfather unchanged URLsGap1.5New comparison logic in ItemService
CSV refactoringGap3.xExtract common S3 capabilities
CloudFormation importsGap4.1Four new exports to import
Helm configurationGap4.2Four new config values
Module wiringGap4.3New service instantiation and DI

Priority 1: Foundation Classes (common-module)

Section titled “Priority 1: Foundation Classes (common-module)”

New classes that everything else depends on. No existing code is modified.

  1. AssetKeyGenerator — simplest class, no AWS dependencies, pure logic.
  2. CdnUrlResolver — pure logic (URL construction + regex validation).
  3. S3AssetService — presigned POST, HEAD, metadata validation. Heaviest class; depends on AWS SDK presigned POST API.

Priority 2: CSV Refactoring (common-module)

Section titled “Priority 2: CSV Refactoring (common-module)”

Extract shared capabilities from CsvS3BucketDirectAccess into S3AssetService. This is the highest-risk change — must not break existing tests. The extraction boundary is a design decision (see open question in Category 3).

CloudFormation imports, Helm values, and Module.kt wiring. These are mechanical changes following established patterns but must be correct for deployment.

ImageUploadEndpoint and ItemService modification. These depend on all Priority 1 classes being available (via includeBuild during development).


Copyright: (c) Arda Systems 2025-2026, All rights reserved