Skip to content

Current State: Upload Product Images and Managed File Assets

Summary of decisions and open items across all design sessions for the Upload Product Images project. Generated after Design Session 03 (in progress).

Business Requirements (from Project Description)

Section titled “Business Requirements (from Project Description)”
  1. General-purpose file upload mechanism — the platform needs a managed S3 bucket workflow for uploading assets referenced by entity fields, served directly to HTTP clients without backend proxying.
  2. First use case: product images — upload PNG, JPEG, or WebP files to be set as Item.imageUrl in the Item module of operations.
  3. Presigned URL workflow — clients upload directly to S3 via presigned credentials; the resulting stable URL is stored on the entity.
  4. CDN delivery — serve images via CloudFront to optimize static asset delivery.
  5. Future extensibility — the design must accommodate user profile images, order document scans, and other entity-attached assets without architectural changes.
  1. Items must support images from either external URLs or uploaded S3 objects, transparently — the read path returns a URL in both cases.
  2. Updates accept an external imageUrl, an imageKey (UUID resolving to an S3 object), or both. When imageKey is present, it must resolve or the operation fails — no fallback to imageUrl.
  3. CSV imports follow the same resolution rules: S3 keys must resolve to pre-uploaded objects or the row fails.
  4. Only the UUID portion of the object key is provided by the client; the system constructs the full key from tenant context and probes allowed extensions.

SPA (S-1 – S-7): Request upload credentials, client-side pre-validation (type + size), construct multipart form from server-provided formFields, upload to S3 via POST, update entity with imageKey, show progress feedback, and support error recovery (retry upload within 15-minute presigned window).

BFF (B-1 – B-4): Proxy upload-url and entity-update requests to operations; no file proxying (SPA uploads directly to S3); forward error responses.

Endpoint (E-1 – E-2): POST /v1/item/item/image/upload-url returns presigned POST credentials (amended from DS-02 to remove {itemId} from path). PUT /v1/item/item/{eId} accepts optional imageKey and performs HEAD validation.

Module (M-1 – M-8): Pre-checks (authorization, entity existence, Content-Type allow-list, file size range 50 KB – 10 MB), delegate to S3BucketAccess for presigned POST generation, HEAD validation on entity update (object exists, Content-Type valid, tenant metadata matches), persist resolved URL, configurable constraints via ModuleConfig.

Library (L-1 – L-8): S3BucketAccess class parametrized at bootstrap (bucket, module, region, credentials, signature duration); methods for generatePresignedPost and headObject; key construction helper; data classes PresignedPostResult (amended to include imageKey: UUID) and S3ObjectMetadata.

CDK (C-1 – C-8): New http-assets bucket (durable, no TTL, SSE-S3, BLOCK_ALL, CORS for SPA origins); IAM grants for PutObject/GetObject/HeadObject; bucket ARN export; existing ephemeral-uploads bucket unchanged.

  • Image processing (resizing, format conversion, thumbnailing).
  • Bulk image upload (batch of multiple images in one operation).
  • UI implementation (frontend upload component — separate project).
  • Migration of existing imageUrl values.
  • Two S3 buckets per partition: http-assets (durable, no TTL) and ephemeral-uploads (existing, 1-day TTL for CSV).
  • Object key format: ${tenantId}/${owning-module}/${entity-type}/${property-name}/${asset-uuid}.${extension}
  • Immutable objects: each upload creates a new key (UUID). Replacing an image means uploading a new object and updating the entity reference. No S3 versioning. Orphan cleanup deferred.
  • Encryption: SSE-S3 (AES-256). Public access blocked (BLOCK_ALL).
  • Separate CloudFront distribution for assets (not shared with API).
  • URL pattern: ${partition}.${infrastructure}.assets.arda.cards
  • Three-phase rollout:
    1. Phase 1: Direct S3 access via presigned URLs or bucket URL, no CDN.
    2. Phase 2: CloudFront distribution without access control (unguessable keys).
    3. Phase 3: CloudFront with tenant-scoped signed cookies (BFF issues cookies).
  • BFF scope extension: arda-frontend-app modifications needed for Phase 3 (cookie issuance via @aws-sdk/cloudfront-signer). Not needed for Phases 1-2.
  • Secrets Manager for CloudFront RSA key pair (Phase 3).
  • Option A (Decoupled Upload + Entity Update) with mandatory HEAD check.
  • Three-step client flow:
    1. Request presigned POST credentials from backend.
    2. Upload directly to S3 (multipart form POST).
    3. Create/update entity with imageKey: UUID.
  • Presigned POST (not PUT) for S3-edge enforcement of Content-Type and Content-Length constraints.
  • Upload endpoint: POST /v1/item/item/image/upload-url (no entity-id in path — amended from DS-02’s original design which included {itemId}).
  • Response includes imageKey: UUID alongside objectKey and formFields.
ConditionValue
keyExact match (server-generated)
Content-Typestarts-with "image/"
content-length-range50 KB — 10 MB
x-amz-meta-tenant-idExact match (from context)
x-amz-meta-authorExact match (from context)
Accepted MIME typesimage/jpeg, image/png, image/webp
Signature duration15 minutes
  • Shared library in common-module (not Lambda). New S3BucketAccess abstraction parallel to existing CsvS3BucketDirectAccess.
  • Strongly typed Storage Access class parametrized at bootstrap with module name, entity type, property name. Runtime access needs only tenantId, objectKey, and extension.
  • Re-evaluate Lambda (DQ-003 Option A) once more upload use cases exist.
  • ItemInput gets imageKey: UUID? (new, optional). imageUrl: String? is retained for external URLs.
  • Item domain entity keeps imageUrl: URL? only. No new persisted field. imageKey is transient — resolved to a URL during ItemInput to Item translation.
  • No persistence schema change. The image_url column stores either an external URL or a resolved S3/CDN URL. They are indistinguishable in the DB.
imageKeyimageUrlS3 Object ExistsResult
absentabsentItem.imageUrl = null
absentpresentItem.imageUrl = provided URL
presentabsentyesItem.imageUrl = resolved URL
presentabsentnoError (422)
presentpresentyesItem.imageUrl = resolved URL
presentpresentnoError (422) — no fallback

When imageKey: UUID is provided, the backend constructs the key prefix {tenantId}/operations/item/image/ and probes extensions in order: .jpg, .png, .webp via S3 HEAD requests until a match is found. If none match, the operation fails with 422.

  • Draft creation (GET /v1/item/item/{eId}/draft): independent of uploads.
  • Draft update (PUT /v1/item/item/{eId}/draft): stores imageKey in draft value without validation.
  • Publish (PUT /v1/item/item/{eId}): resolves and validates imageKey via HEAD check. This is the standard update path — draft promotion is implicit (the backend verifies a draft exists, closes it, and updates the universe).
  • Item creation (POST /v1/item/item): resolves and validates imageKey immediately. No draft involved.

The resolveImageKey method is called during ItemInput to Item translation, which occurs in both the create and update code paths. This is the same location where imageUrl: String? is currently converted to URL? via URI(it).toURL() in ItemInput.toItem(eId).

RepositoryRoleChanges Expected
common-moduleS3 access abstraction (S3BucketAccess)New classes in lib/infra/storage/
operationsItem module: endpoint, service, modelNew upload route, model change, resolver
infrastructureS3 bucket creation, CloudFront, IAMNew CDK constructs and stacks
arda-frontend-appSPA upload UI + BFF proxy routesPhase 1: SPA changes. Phase 3: BFF cookies
api-testAPI verificationNew Bruno test collections
ItemStatusNotes
DQ-008 (immutability)Pending Denis reviewOption A agreed in principle, awaiting confirmation
Object eviction requirementsDeferredSeparate session

CSV Import (DS-03, Step 3 — Questions Raised, Not Yet Decided)

Section titled “CSV Import (DS-03, Step 3 — Questions Raised, Not Yet Decided)”
QuestionSummaryRecommendation
CSV-Q1How to represent S3 keys in CSV columns?New image_key column (separate from image_url)
CSV-Q2Pre-upload workflow (images must exist before CSV processed)Sequential: upload N images, then CSV
CSV-Q3Performance at scale (up to 3 HEAD calls per row)Consider ListObjectsV2 for batch path
CSV-Q4Error handling (per-row vs batch failure)Per-row failure, descriptive error
CSV-Q5Mutual exclusivity if both columns populatedSame rules as API (imageKey precedence, fail if unresolvable)
DeliverableStatus
Current SPA interaction diagrams (Step 0)Done
Item creation with upload diagram (Step 1)Done
Draft lifecycle with upload diagram (Step 2)Done
CSV import questions (Step 3)Done
CSV import scenario diagramBlocked on CSV-Q1—Q5 answers
Ktor route specs / Endpoint DSL pseudo-codeNot started
Object key validation scenarios (API + CSV)Not started
S3 Access abstraction Kotlin interfaceNot started

Amendments to Prior Documents (Actions from DS-03)

Section titled “Amendments to Prior Documents (Actions from DS-03)”
#TargetChange
A-1DS-02, E-1Remove {itemId} from upload endpoint path
A-2DS-02, L-2 / L-6Add imageKey: UUID to PresignedPostResult
A-3DS-02, Resulting Design”Both provided” case: fail if imageKey unresolvable, no fallback
A-4Decision LogUpdate DQ-005 with corrected endpoint path and response fields
A-5DS-03, OQ-3Confirmed: /v1/item/item/ prefix convention for upload endpoint

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