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”Executive Summary
Section titled “Executive Summary”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)”1.1 Presigned POST Credential Generation
Section titled “1.1 Presigned POST Credential Generation”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.
1.2 CDN URL Construction and Validation
Section titled “1.2 CDN URL Construction and Validation”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).
1.3 Tenant-Scoped Asset Key Generation
Section titled “1.3 Tenant-Scoped Asset Key Generation”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.
1.4 Image Upload REST Endpoint
Section titled “1.4 Image Upload REST Endpoint”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
inputTranslator → ItemInput.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).
Category 2: Behavioral Differences
Section titled “Category 2: Behavioral Differences”2.1 Presigned PUT vs. POST
Section titled “2.1 Presigned PUT vs. POST”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.
2.2 Metadata Enforcement Pattern
Section titled “2.2 Metadata Enforcement Pattern”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.
2.3 Key Construction Responsibility
Section titled “2.3 Key Construction Responsibility”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:
| # | Capability | Current Location | Extraction Target |
|---|---|---|---|
| 3.1 | S3Presigner creation (region + credentials) | CsvS3ObjectDirectService.kt:150-153 | S3AssetService constructor |
| 3.2 | S3AsyncClient creation with config | CsvS3ObjectDirectService.kt:119-137 | S3AssetService factory or shared builder |
| 3.3 | HEAD object request | CsvS3ObjectDirectService.kt:212-232 | S3AssetService.headObject() |
| 3.4 | Metadata validation (required + matching) | CsvS3ObjectDirectService.kt:187-210 | S3AssetService.validateMetadata() |
| 3.5 | Error wrapping (AppError.ExternalService) | CsvS3ObjectDirectService.kt:224-231 | Shared error handling in S3AssetService |
| 3.6 | Signature duration configuration | CsvS3ObjectDirectService.kt:111 | S3AssetService constructor parameter |
| 3.7 | Presigned PUT URL generation | CsvS3ObjectDirectService.kt:169-185 | S3AssetService.presignedPutUrl() (TD-17) |
Not extractable (CSV-specific, remains in CsvS3BucketDirectAccess):
| # | Capability | Reason |
|---|---|---|
| N.1 | GET object with decompression | CSV-specific (GZIP/BZIP2 decompression) |
| N.2 | CSV row/batch flow parsing | CSV-specific (kotlin-csv, header normalization) |
| N.3 | Compression type handling | CSV-specific (CompressionType enum, file suffix) |
| N.4 | File 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.imageAssetBucketArnsystem.reference.item.extras.imageAssetBucketNamesystem.reference.item.extras.imagePresignRoleArnsystem.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.
Summary Table
Section titled “Summary Table”| Specification Requirement | Status | Category | Notes |
|---|---|---|---|
| BE-FR-001: Presigned POST generation | Gap | 1.1 | New capability — no POST support exists |
| BE-FR-002: Tenant-scoped key construction | Gap | 1.3 | New AssetKeyGenerator class |
| BE-FR-003: CDN URL validation | Gap | 1.2 | New CdnUrlResolver class |
| BE-FR-004: HEAD verification before persist | Gap | 1.5 | New validation step in ItemService |
| BE-FR-005: Null imageUrl clears field | OK | — | Already works — null propagates through |
| BE-FR-006: Bitemporal version history | OK | — | Universe handles this automatically |
| BE-FR-007: Return CDN URL with presigned form | Gap | 1.4 | New ImageUploadEndpoint response |
| BE-NFR-001: Presigning < 1s P95 | Gap | — | Untested — depends on implementation |
| BE-NFR-002: IAM scoped to bucket+prefix | Gap | 4.1 | CloudFormation policy needed |
| BE-NFR-003: Idempotent endpoint | Gap | 1.4 | Each call generates new UUID/credentials |
| TD-15: Grandfather unchanged URLs | Gap | 1.5 | New comparison logic in ItemService |
| CSV refactoring | Gap | 3.x | Extract common S3 capabilities |
| CloudFormation imports | Gap | 4.1 | Four new exports to import |
| Helm configuration | Gap | 4.2 | Four new config values |
| Module wiring | Gap | 4.3 | New service instantiation and DI |
Prioritized Recommendations
Section titled “Prioritized Recommendations”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.
AssetKeyGenerator— simplest class, no AWS dependencies, pure logic.CdnUrlResolver— pure logic (URL construction + regex validation).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).
Priority 3: Operations Infrastructure
Section titled “Priority 3: Operations Infrastructure”CloudFormation imports, Helm values, and Module.kt wiring. These are mechanical changes following established patterns but must be correct for deployment.
Priority 4: Operations Business Logic
Section titled “Priority 4: Operations Business Logic”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
Copyright: © Arda Systems 2025-2026, All rights reserved