Skip to content

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).

IDRequirementScenariosSource
BE-FR-001Generate 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, S7FR-020, FR-024, TD-03, TD-08
BE-FR-002Construct 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, S7NFR-007, DQ-3
BE-FR-003Validate 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, S7FR-012, NFR-005, TD-05
BE-FR-004Before 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, S7FR-023
BE-FR-005Accept null as a valid imageUrl value to clear an entity’s image (remove image flow).S3, S7FR-025, FR-026
BE-FR-006Retain previous image references in the entity’s bitemporal version history. Do not delete managed assets on image replacement or removal.S1, S2, S3, S7FR-027
BE-FR-007Return 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, S2FR-022
BE-FR-008Merged into BE-FR-004. Existence verification and metadata validation are performed in a single HeadObject call.
IDRequirementSource
BE-NFR-001Presigned POST credential generation shall complete within 1 second (P95).NFR-003
BE-NFR-002The presigning IAM role shall be scoped to the specific S3 bucket and key prefix for the requesting tenant.NFR-006
BE-NFR-003The upload-url endpoint shall be idempotent — multiple calls produce independent presigned forms with distinct object keys.Derived from retry robustness

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:

    1. Extract tenant ID and author from request context.
    2. Generate UUID for object key.
    3. Construct S3 key: <tenantId>/images/<uuid>.<ext>.
    4. Construct Arda-Key: operations/item/imageUrl/<uuid>.<ext>.
    5. Assume presigning role via sts:AssumeRole.
    6. Generate presigned POST form with policy conditions (key, Content-Type, content-length-range, tenant-id, author, arda-key, SSE). Duration: 15 min.
    7. Construct CDN URL: https://<cdn-host>/<tenantId>/images/<uuid>.<ext>.
    8. 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 formFields as 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 fields
    • 401 — authentication failure
    • 500 — presigning failure (IAM role assumption, S3 client error)

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 or null).
  • Backend action (image-specific):
    1. If imageUrl is 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.
    2. If imageUrl is null: clear the image field (BE-FR-005).
    3. Persist entity via bitemporal Universe (BE-FR-006).
  • Errors:
    • 400 — imageUrl does not match CDN pattern, wrong tenant prefix, or object does not exist in Storage
    • 404 — entity not found
OperationPurposeScenario
createPresignedPost()Generate presigned POST form with policyS1, S2 (credential generation)
headObject()Verify uploaded object exists before persisting URLS1, S2 (entity persist step)

The Item entity already has imageUrl: URL? at every persistence layer:

LayerFieldTypeLocation
Business EntityimageUrlURL?item/business/Item.kt
API Input ModelimageUrlString?item/api/Model.kt
Persistenceimage_urlurl().nullable()item/persistence/ItemPersistence.kt
CSV Protoimage_urlstring (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 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).


ModulePackagePurposeScenarios
S3AssetServicecards.arda.common.lib.infra.storageGeneral-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
AssetKeyGeneratorcards.arda.common.lib.infra.storageConstructs tenant-scoped S3 keys in the format <tenantId>/images/<uuid>.<ext>. Encapsulates the key convention.S1, S2
CdnUrlResolvercards.arda.common.lib.infra.storageConstructs CDN URLs from object keys. Validates that a given URL matches the expected CDN host and key pattern.S1, S2, S3 (validation)
ModulePackagePurposeScenarios
ImageUploadEndpointcards.arda.operations.reference.item.api.restREST endpoint for POST /v1/item/<itemEId>/image-upload-url. Delegates to S3AssetService. Scoped under the Item resource path (TD-13).S1, S2
ModulePackageChangeScenarios
ItemServicecards.arda.operations.reference.item.businessAdd imageUrl validation step before persist: call CdnUrlResolver.validate() and S3AssetService.headObject().S1, S2, S3
ItemEndpointcards.arda.operations.reference.item.api.restNo structural change — imageUrl is already in the API model. Validation is in the service layer.
TestTargetInfrastructureValidates
S3AssetServiceTestS3AssetServiceMockAWS (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
AssetKeyGeneratorTestAssetKeyGeneratorUnit (no infra)Key format, tenant prefix, UUID uniqueness
CdnUrlResolverTestCdnUrlResolverUnit (no infra)URL construction, pattern validation, tenant isolation, null handling
ImageUploadEndpointTestImageUploadEndpointIntegration (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