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).
Requirements Summary
Section titled “Requirements Summary”Business Requirements (from Project Description)
Section titled “Business Requirements (from Project Description)”- 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.
- First use case: product images — upload PNG, JPEG, or WebP files to be
set as
Item.imageUrlin the Item module ofoperations. - Presigned URL workflow — clients upload directly to S3 via presigned credentials; the resulting stable URL is stored on the entity.
- CDN delivery — serve images via CloudFront to optimize static asset delivery.
- Future extensibility — the design must accommodate user profile images, order document scans, and other entity-attached assets without architectural changes.
Design Intent (from DS-03)
Section titled “Design Intent (from DS-03)”- Items must support images from either external URLs or uploaded S3 objects, transparently — the read path returns a URL in both cases.
- Updates accept an external
imageUrl, animageKey(UUID resolving to an S3 object), or both. WhenimageKeyis present, it must resolve or the operation fails — no fallback toimageUrl. - CSV imports follow the same resolution rules: S3 keys must resolve to pre-uploaded objects or the row fails.
- 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.
Detailed Requirements (from DS-02)
Section titled “Detailed Requirements (from DS-02)”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.
Out of Scope
Section titled “Out of Scope”- 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
imageUrlvalues.
Decided Architecture
Section titled “Decided Architecture”S3 Storage (DQ-001, DQ-008)
Section titled “S3 Storage (DQ-001, DQ-008)”- Two S3 buckets per partition:
http-assets(durable, no TTL) andephemeral-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).
CDN / Read Path (DQ-002, DQ-006)
Section titled “CDN / Read Path (DQ-002, DQ-006)”- Separate CloudFront distribution for assets (not shared with API).
- URL pattern:
${partition}.${infrastructure}.assets.arda.cards - Three-phase rollout:
- Phase 1: Direct S3 access via presigned URLs or bucket URL, no CDN.
- Phase 2: CloudFront distribution without access control (unguessable keys).
- Phase 3: CloudFront with tenant-scoped signed cookies (BFF issues cookies).
- BFF scope extension:
arda-frontend-appmodifications 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).
Write Path / Upload Workflow (DQ-005)
Section titled “Write Path / Upload Workflow (DQ-005)”- Option A (Decoupled Upload + Entity Update) with mandatory HEAD check.
- Three-step client flow:
- Request presigned POST credentials from backend.
- Upload directly to S3 (multipart form POST).
- 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: UUIDalongsideobjectKeyandformFields.
S3 POST Policy Constraints
Section titled “S3 POST Policy Constraints”| Condition | Value |
|---|---|
key | Exact match (server-generated) |
Content-Type | starts-with "image/" |
content-length-range | 50 KB — 10 MB |
x-amz-meta-tenant-id | Exact match (from context) |
x-amz-meta-author | Exact match (from context) |
| Accepted MIME types | image/jpeg, image/png, image/webp |
| Signature duration | 15 minutes |
Service Architecture (DQ-003, DQ-007)
Section titled “Service Architecture (DQ-003, DQ-007)”- Shared library in
common-module(not Lambda). NewS3BucketAccessabstraction parallel to existingCsvS3BucketDirectAccess. - Strongly typed Storage Access class parametrized at bootstrap with module
name, entity type, property name. Runtime access needs only
tenantId,objectKey, andextension. - Re-evaluate Lambda (DQ-003 Option A) once more upload use cases exist.
Design Session 03 Decisions (In Progress)
Section titled “Design Session 03 Decisions (In Progress)”Field Strategy
Section titled “Field Strategy”ItemInputgetsimageKey: UUID?(new, optional).imageUrl: String?is retained for external URLs.Itemdomain entity keepsimageUrl: URL?only. No new persisted field.imageKeyis transient — resolved to a URL duringItemInputtoItemtranslation.- No persistence schema change. The
image_urlcolumn stores either an external URL or a resolved S3/CDN URL. They are indistinguishable in the DB.
Resolution Rules
Section titled “Resolution Rules”imageKey | imageUrl | S3 Object Exists | Result |
|---|---|---|---|
| absent | absent | — | Item.imageUrl = null |
| absent | present | — | Item.imageUrl = provided URL |
| present | absent | yes | Item.imageUrl = resolved URL |
| present | absent | no | Error (422) |
| present | present | yes | Item.imageUrl = resolved URL |
| present | present | no | Error (422) — no fallback |
Extension Probing
Section titled “Extension Probing”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 Lifecycle Integration
Section titled “Draft Lifecycle Integration”- Draft creation (
GET /v1/item/item/{eId}/draft): independent of uploads. - Draft update (
PUT /v1/item/item/{eId}/draft): storesimageKeyin draft value without validation. - Publish (
PUT /v1/item/item/{eId}): resolves and validatesimageKeyvia 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 validatesimageKeyimmediately. No draft involved.
Integration Point
Section titled “Integration Point”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).
Repositories Involved
Section titled “Repositories Involved”| Repository | Role | Changes Expected |
|---|---|---|
common-module | S3 access abstraction (S3BucketAccess) | New classes in lib/infra/storage/ |
operations | Item module: endpoint, service, model | New upload route, model change, resolver |
infrastructure | S3 bucket creation, CloudFront, IAM | New CDK constructs and stacks |
arda-frontend-app | SPA upload UI + BFF proxy routes | Phase 1: SPA changes. Phase 3: BFF cookies |
api-test | API verification | New Bruno test collections |
Open Items
Section titled “Open Items”Pending Decisions
Section titled “Pending Decisions”| Item | Status | Notes |
|---|---|---|
| DQ-008 (immutability) | Pending Denis review | Option A agreed in principle, awaiting confirmation |
| Object eviction requirements | Deferred | Separate 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)”| Question | Summary | Recommendation |
|---|---|---|
| CSV-Q1 | How to represent S3 keys in CSV columns? | New image_key column (separate from image_url) |
| CSV-Q2 | Pre-upload workflow (images must exist before CSV processed) | Sequential: upload N images, then CSV |
| CSV-Q3 | Performance at scale (up to 3 HEAD calls per row) | Consider ListObjectsV2 for batch path |
| CSV-Q4 | Error handling (per-row vs batch failure) | Per-row failure, descriptive error |
| CSV-Q5 | Mutual exclusivity if both columns populated | Same rules as API (imageKey precedence, fail if unresolvable) |
DS-03 Remaining Deliverables (OQ-6)
Section titled “DS-03 Remaining Deliverables (OQ-6)”| Deliverable | Status |
|---|---|
| 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 diagram | Blocked on CSV-Q1—Q5 answers |
| Ktor route specs / Endpoint DSL pseudo-code | Not started |
| Object key validation scenarios (API + CSV) | Not started |
| S3 Access abstraction Kotlin interface | Not started |
Amendments to Prior Documents (Actions from DS-03)
Section titled “Amendments to Prior Documents (Actions from DS-03)”| # | Target | Change |
|---|---|---|
| A-1 | DS-02, E-1 | Remove {itemId} from upload endpoint path |
| A-2 | DS-02, L-2 / L-6 | Add imageKey: UUID to PresignedPostResult |
| A-3 | DS-02, Resulting Design | ”Both provided” case: fail if imageKey unresolvable, no fallback |
| A-4 | Decision Log | Update DQ-005 with corrected endpoint path and response fields |
| A-5 | DS-03, OQ-3 | Confirmed: /v1/item/item/ prefix convention for upload endpoint |
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved