Backend Specification
Sub-system specification for the Backend — the Kotlin/Ktor business services
in the operations component and common-module shared library. The Backend is
responsible for presigned POST credential generation, entity persistence with
URL validation, and upload verification. For the structural overview of all
sub-systems and their dependencies, see design.md.
The Backend does not handle image file bytes (TD-06). It brokers credentials and metadata only (TD-03).
Actor Requirements
Section titled “Actor Requirements”Functional
Section titled “Functional”| ID | Requirement | Scenarios | Source |
|---|---|---|---|
| BE-FR-001 | Generate presigned POST forms for uploading images to Storage (TD-08). The POST policy document shall include conditions enforcing: key (exact match), Content-Type (starts-with image/), content-length-range (1 byte to max file size), x-amz-meta-tenant-id (exact match to requesting tenant), x-amz-meta-author (exact match to requesting user), x-amz-meta-arda-key (exact match), and x-amz-server-side-encryption (AES256). Signature duration: 15 minutes (configurable, aligned with existing CSV upload uploadSignatureDuration). The metadata elements follow the same pattern as CsvS3BucketDirectAccess (tenant-id, author) adapted to POST policy conditions. | S1, S2, S5, S6, S7 | FR-020, FR-024, TD-03, TD-08 |
| BE-FR-002 | Construct S3 object keys in the format <tenantId>/images/<uuid>.<ext>. The UUID is generated server-side. The extension is derived from the requested content type. | S1, S2, S5, S6, S7 | NFR-007, DQ-3 |
| BE-FR-003 | Validate that all imageUrl values submitted for entity persistence match the expected CDN host and key pattern. Reject URLs that do not match. Backward-compatibility note (TD-15, TD-21): validation is skipped when the imageUrl value is unchanged from the currently persisted value, allowing pre-existing non-CDN URLs to remain valid until explicitly changed. | S1, S2, S3, S5, S6, S7 | FR-012, NFR-005, TD-05 |
| BE-FR-004 | Before persisting an imageUrl on an entity, perform a HeadObject request to Storage to verify the uploaded object exists and validate that x-amz-meta-tenant-id matches the requesting tenant (defense-in-depth, following the CsvUploadService.processCsv matchingAttributes pattern). This is a single HeadObject call that satisfies both existence verification and metadata validation. | S1, S2, S5, S6, S7 | FR-023 |
| BE-FR-005 | Accept null as a valid imageUrl value to clear an entity’s image (remove image flow). | S3, S7 | FR-025, FR-026 |
| BE-FR-006 | Retain previous image references in the entity’s bitemporal version history. Do not delete managed assets on image replacement or removal. | S1, S2, S3, S7 | FR-027 |
| BE-FR-007 | Return the CDN URL (constructed from the object key and CDN host) alongside the presigned POST form fields so the SPA can persist the final entity URL without additional round-trips. | S1, S2 | FR-022 |
| — | — |
Non-Functional
Section titled “Non-Functional”| ID | Requirement | Source |
|---|---|---|
| BE-NFR-001 | Presigned POST credential generation shall complete within 1 second (P95). | NFR-003 |
| BE-NFR-002 | The presigning IAM role shall be scoped to the specific S3 bucket and key prefix for the requesting tenant. | NFR-006 |
| BE-NFR-003 | The upload-url endpoint shall be idempotent — multiple calls produce independent presigned forms with distinct object keys. | Derived from retry robustness |
Interfaces
Section titled “Interfaces”Inbound: BFF → Backend REST API
Section titled “Inbound: BFF → Backend REST API”POST /v1/item/<itemEId>/image-upload-url
Section titled “POST /v1/item/<itemEId>/image-upload-url”Generate presigned POST credentials for uploading an image to this item. The entity type and identity are conveyed by the path (TD-13).
-
Authentication: API key + tenant headers (
X-Tenant-Id,X-Author,X-Request-ID) -
Request body:
{"contentType": "image/jpeg","contentLength": 245000} -
Backend action:
- Extract tenant ID and author from request context.
- Generate UUID for object key.
- Construct S3 key:
<tenantId>/images/<uuid>.<ext>. - Construct Arda-Key:
operations/item/imageUrl/<uuid>.<ext>. - Assume presigning role via
sts:AssumeRole. - Generate presigned POST form with policy conditions (key, Content-Type, content-length-range, tenant-id, author, arda-key, SSE). Duration: 15 min.
- Construct CDN URL:
https://<cdn-host>/<tenantId>/images/<uuid>.<ext>. - Return form fields, upload URL, object key, and CDN URL.
-
Response:
{"uploadUrl": "https://<bucket>.s3.<region>.amazonaws.com","formFields": {"key": "<tenantId>/images/<uuid>.jpg","Content-Type": "image/jpeg","x-amz-meta-tenant-id": "<tenantId>","x-amz-meta-author": "<authorId>","x-amz-meta-arda-key": "operations/item/imageUrl/<uuid>.jpg","x-amz-server-side-encryption": "AES256","X-Amz-Credential": "...","X-Amz-Algorithm": "AWS4-HMAC-SHA256","X-Amz-Date": "...","Policy": "<base64-encoded policy with all conditions>","X-Amz-Signature": "..."},"objectKey": "<tenantId>/images/<uuid>.jpg","cdnUrl": "https://<cdn-host>/<tenantId>/images/<uuid>.jpg"}The SPA submits all
formFieldsas multipart form data alongside the file (which must be the last field). The metadata values (tenant-id,author,arda-key) and encryption requirement are baked into the signed policy — the SPA includes them as-is without modification. -
Errors:
400— invalid content type or missing required fields401— authentication failure500— presigning failure (IAM role assumption, S3 client error)
PUT /v1/item/<itemEId>
Section titled “PUT /v1/item/<itemEId>”Update item entity (existing endpoint — extended with image URL validation).
- Authentication: API key + tenant headers
- Request body: Full item payload including
imageUrl(CDN URL string ornull). - Backend action (image-specific):
- If
imageUrlis non-null: a. Validate URL matches expected CDN host and key pattern (BE-FR-003). b. Validate URL key prefix matches requesting tenant (tenant isolation). c. HEAD request to Storage to verify object exists (BE-FR-004). d. If any validation fails, reject with 400. - If
imageUrlis null: clear the image field (BE-FR-005). - Persist entity via bitemporal Universe (BE-FR-006).
- If
- Errors:
400— imageUrl does not match CDN pattern, wrong tenant prefix, or object does not exist in Storage404— entity not found
Outbound: Backend → Storage (AWS SDK)
Section titled “Outbound: Backend → Storage (AWS SDK)”| Operation | Purpose | Scenario |
|---|---|---|
createPresignedPost() | Generate presigned POST form with policy | S1, S2 (credential generation) |
headObject() | Verify uploaded object exists before persisting URL | S1, S2 (entity persist step) |
Entity Persistence
Section titled “Entity Persistence”The Item entity already has imageUrl: URL? at every persistence layer:
| Layer | Field | Type | Location |
|---|---|---|---|
| Business Entity | imageUrl | URL? | item/business/Item.kt |
| API Input Model | imageUrl | String? | item/api/Model.kt |
| Persistence | image_url | url().nullable() | item/persistence/ItemPersistence.kt |
| CSV Proto | image_url | string (URI validated) | item/csv/v1beta1/item_row.proto |
No schema changes are required. The field is fully wired — this design adds the validation logic (CDN pattern match) and the presigned credential generation capability.
Presigned Credential State
Section titled “Presigned Credential State”Presigned POST credentials are stateless from the Backend’s perspective. Each request generates a new credential set. There is no server-side session or tracking of issued credentials. Orphaned uploads (credentials used, entity never updated) remain in Storage; cleanup is deferred (see NFR-012).
Modules
Section titled “Modules”New Modules in common-module
Section titled “New Modules in common-module”| Module | Package | Purpose | Scenarios |
|---|---|---|---|
S3AssetService | cards.arda.common.lib.infra.storage | General-purpose S3 presigned POST generation, object key construction, HEAD verification, and post-upload metadata validation. Complements the existing CsvS3BucketDirectAccess. Follows the same metadata pattern (tenant-id, author) adapted from signed PUT headers to POST policy conditions. Assumes the presigning IAM role the same way CsvS3BucketDirectAccess uses S3Presigner. | S1, S2 |
AssetKeyGenerator | cards.arda.common.lib.infra.storage | Constructs tenant-scoped S3 keys in the format <tenantId>/images/<uuid>.<ext>. Encapsulates the key convention. | S1, S2 |
CdnUrlResolver | cards.arda.common.lib.infra.storage | Constructs CDN URLs from object keys. Validates that a given URL matches the expected CDN host and key pattern. | S1, S2, S3 (validation) |
New Modules in operations
Section titled “New Modules in operations”| Module | Package | Purpose | Scenarios |
|---|---|---|---|
ImageUploadEndpoint | cards.arda.operations.reference.item.api.rest | REST endpoint for POST /v1/item/<itemEId>/image-upload-url. Delegates to S3AssetService. Scoped under the Item resource path (TD-13). | S1, S2 |
Modified Modules
Section titled “Modified Modules”| Module | Package | Change | Scenarios |
|---|---|---|---|
ItemService | cards.arda.operations.reference.item.business | Add imageUrl validation step before persist: call CdnUrlResolver.validate() and S3AssetService.headObject(). | S1, S2, S3 |
ItemEndpoint | cards.arda.operations.reference.item.api.rest | No structural change — imageUrl is already in the API model. Validation is in the service layer. | — |
Testing Strategy
Section titled “Testing Strategy”| Test | Target | Infrastructure | Validates |
|---|---|---|---|
S3AssetServiceTest | S3AssetService | MockAWS (LocalStack S3) | Presigned POST form generation (policy conditions include tenant-id, author, arda-key, SSE, content-length-range, Content-Type), key construction, HEAD verification, post-upload metadata validation |
AssetKeyGeneratorTest | AssetKeyGenerator | Unit (no infra) | Key format, tenant prefix, UUID uniqueness |
CdnUrlResolverTest | CdnUrlResolver | Unit (no infra) | URL construction, pattern validation, tenant isolation, null handling |
ImageUploadEndpointTest | ImageUploadEndpoint | Integration (Harness + MockAWS) | Full endpoint: auth, presigned generation, response format. Path: POST /v1/item/<itemEId>/image-upload-url |
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved