Skip to content

AWS Specification

Sub-system specification for Storage (AWS Resources) — the cloud infrastructure supporting image storage and delivery. This document identifies specific AWS services, configurations, and IAM policies. In scenario diagrams these resources appear as “Storage” (a single black-box participant per TD-07); the details below break that abstraction. For the structural overview of all sub-systems and their dependencies, see design.md.

IDRequirementScenariosSource
AWS-FR-001Provide a persistent S3 bucket for image assets (no TTL expiration). Separate from the existing ephemeral upload bucket (1-day TTL for CSV processing).S1, S2, S5, S6, S7DQ-2, prior research
AWS-FR-002S3 object keys shall follow the format <tenantId>/images/<uuid>.<ext> for tenant isolation and CloudFront path routing.All uploadNFR-007, BE-FR-002, DQ-3
AWS-FR-003Support presigned POST uploads with server-side policy enforcement: Content-Type starts-with image/, Content-Length between 1 byte and max file size, key matches tenant prefix.S1, S2FR-024, TD-08
AWS-FR-004Provide a CloudFront distribution with OAC (Origin Access Control) for serving images from the S3 bucket. The bucket shall not allow direct public access. CloudFront shall require valid signed cookies for all requests; unauthenticated requests return 403. Signed cookie policy shall scope access to the requesting tenant’s key prefix.S4 (read), all displayNFR-014, NFR-017, NFR-018, DQ-6
AWS-FR-005Apply a lifecycle rule on the staging/ prefix (if used) to expire orphaned uploads after 7 days. Apply a lifecycle rule to abort incomplete multipart uploads after 1 day.— (operational)NFR-011, NFR-012
AWS-FR-006The S3 bucket shall be encrypted with SSE-S3 (Amazon managed keys) and have versioning enabled.— (operational)Prior design: Static Asset Repository
AWS-FR-007Block all public access to the S3 bucket. Only CloudFront (via OAC) and the Backend presigning role shall have access.— (security)DQ-1, NFR-005
IDRequirementSource
AWS-NFR-001CDN-served images shall achieve P95 latency under 500 ms for cached content in US/EU regions.NFR-002
AWS-NFR-002The infrastructure shall support at least 50 GB of image storage without configuration changes.NFR-013
AWS-NFR-003S3 bucket removal policy shall be RETAIN — the bucket survives CDK stack deletion.Prior design: Static Asset Repository
AWS-NFR-004The CloudFront signing key pair shall be rotatable without downtime. The trusted key group supports multiple active keys for zero-downtime rotation.NFR-020

The S3 object key is the primary interface contract between the Backend (which constructs keys and generates presigned credentials), the SPA (which uploads to a specific key), and CloudFront (which routes and authorizes based on key prefix). All actors must agree on the key format.

<tenantId>/images/<uuid>.<ext>
SegmentSourceExamplePurpose
<tenantId>Extracted from authenticated session context (Backend)1fa48bf2-3ef9-4d08-8858-29e71504a1edTenant isolation. Partitions storage by tenant. CloudFront signed cookie policy is scoped to /<tenantId>/*. IAM policies can be scoped to this prefix. S3 prefix-based lifecycle rules can target specific tenants if needed.
imagesFixed literalimagesFeature namespace. Distinguishes image assets from other asset types that may share the bucket in the future (e.g., documents, exports). Enables prefix-based lifecycle rules and CloudFront cache behaviors per feature.
<uuid>Generated server-side by AssetKeyGenerator (Backend)a7c3e2f1-9b4d-4e8a-b6c5-d0f1e2a3b4c5Uniqueness and immutability. UUID v4 provides 122 bits of entropy — no collisions, no guessability. A new UUID is generated for every upload, including replacements. The old key’s object remains in S3 (retained for version history); the entity’s imageUrl field is updated to point to the new key.
<ext>Derived from the requested contentType by AssetKeyGeneratorjpg, png, webp, heicContent-type hint. Allows S3 and CloudFront to set correct Content-Type response headers without content inspection. Mapping: image/jpegjpg, image/pngpng, image/webpwebp, image/heicheic, image/heifheif.
1fa48bf2-3ef9-4d08-8858-29e71504a1ed/images/a7c3e2f1-9b4d-4e8a-b6c5-d0f1e2a3b4c5.jpg

The CDN URL is constructed by prepending the CloudFront distribution domain:

https://demo.alpha001.assets.arda.cards/1fa48bf2-.../images/a7c3e2f1-....jpg

The CdnUrlResolver class in common-module constructs this URL from the object key and the CDN domain (injected via environment variable). It also validates that a given URL matches this pattern — used by the Backend to reject any imageUrl that does not originate from managed storage (TD-05, NFR-005).

DecisionRationale
Tenant-first (not feature-first)Tenant ID as the top-level prefix enables IAM policy scoping per tenant, CloudFront signed cookie scoping per tenant (TD-11), and future per-tenant lifecycle rules or quotas. Feature-first (images/<tenantId>/...) would require more complex IAM conditions.
No entity ID in keyThe key does not include the entity ID (e.g., item ID). This decouples the storage key from the entity lifecycle — an image can be uploaded before the entity is created (S6: item creation flow). The link between image and entity is the imageUrl field on the entity, not the S3 key.
UUID per upload, not per entityEach upload generates a new UUID, even for replacements. This makes objects effectively immutable — CloudFront can cache aggressively with long TTLs (no invalidation needed on replacement). The old object remains for version history (FR-027).
Extension in keyIncluding the file extension enables correct Content-Type headers without post-upload metadata inspection. S3 can infer content type from extension when the presigned POST does not set it explicitly.
  • The Backend must generate the key server-side. The SPA never constructs keys — it receives the key (and the presigned form) from the Backend.
  • The tenant ID segment must match the authenticated tenant from the request context. The Backend must not accept a client-supplied tenant ID for key construction.
  • The UUID must be generated using a cryptographically secure random source (e.g., java.util.UUID.randomUUID()).
ConsumerUsage
AssetKeyGenerator (common-module)Constructs keys in this format
CdnUrlResolver (common-module)Constructs CDN URLs from keys; validates URLs match this pattern
ImageUploadEndpoint (operations)Returns the key to the BFF/SPA alongside presigned form fields
Presigned POST formKey and metadata are embedded as form fields; POST policy conditions enforce constraints
CloudFront signed cookieResource field scoped to /<tenantId>/*
S3 lifecycle rulesCan target staging/ prefix or specific tenant prefixes

Bucket name convention: <partition>-<purpose>-image-assets (e.g., alpha001-prod-partition-image-assets)

OperationPathAuthPurpose
POSThttps://<bucket>.s3.<region>.amazonaws.comPresigned POST form with policy documentUpload image bytes (supports multipart)

The Backend generates a presigned POST form using the AWS SDK’s CreatePresignedPost operation. The form includes a policy document with conditions that S3 validates server-side. This approach is chosen over presigned PUT because POST enables content-length-range enforcement and multipart upload support (TD-08).

Policy conditions (S3 validates server-side — upload rejected if any condition fails):

ConditionTypeValuePurpose
keyeq<tenantId>/images/<uuid>.<ext>Exact key match — upload cannot target a different key
Content-Typestarts-withimage/Only image content types accepted
content-length-rangerange[1, <maxFileSizeBytes>]Server-side file size enforcement (e.g., 1 byte to 10 MB)
x-amz-meta-tenant-ideq<tenantId>Tenant ID baked into the policy — client cannot alter
x-amz-meta-authoreq<authorId>Author baked into the policy — client cannot alter
x-amz-meta-arda-keyeqoperations/item/imageUrl/<uuid>.<ext>Arda-Key convention baked into the policy
x-amz-server-side-encryptioneqAES256Enforce SSE-S3 encryption on upload
Expirytimestamp15 minutes from generationAligned with existing CSV upload uploadSignatureDuration

Metadata elements (adapted from the existing CSV upload pattern in CsvS3BucketDirectAccess): The existing CSV upload uses x-amz-meta-tenant-id and x-amz-meta-author as SigV4 signed headers on its presigned PUT-based workflow. This design carries the same metadata elements forward, adapted to presigned POST policy conditions (TD-08). The enforcement is equivalent: the client must include these exact form fields or S3 rejects the upload. Additionally, x-amz-meta-arda-key follows the Static Asset Repository Arda-Key convention.

Form fields returned to SPA (the SPA submits these as multipart form fields alongside the file):

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 document>
X-Amz-Signature = ...
file = <image bytes> ← must be the last field

Signature duration: 15 minutes (configurable via uploadSignatureDuration, aligned with the existing CSV upload pattern).

OperationPurposeIAM Permission
s3:PutObject (presigned POST generation)Generate presigned POST formsVia presigning role
s3:GetObject (HEAD)Verify uploaded object exists; validate metadata post-uploadVia pod service account role
s3:ListBucketList objects for future admin/cleanup operationsVia pod service account role

Post-upload metadata validation: After the SPA reports a successful upload, the Backend performs a HeadObject on the key and validates that x-amz-meta-tenant-id matches the requesting tenant. This follows the same pattern as CsvUploadService.processCsv which validates matchingAttributes against the expected tenant-id and author after CSV upload. Defense-in-depth even though the POST policy conditions already prevent tampering.

OperationPurposeAuth
s3:GetObjectServe images to CDNOrigin Access Control (SigV4)

Distribution purpose: Serve image assets from S3 with caching and edge delivery.

SettingValueRationale
OriginS3 bucket (OAC)Secure origin access without public bucket
Origin Access ControlSigV4 signingModern replacement for OAI
Viewer access restrictionTrusted key groups (signed cookies)Product images are sensitive (TD-11). All viewer requests must carry valid CloudFront signed cookies. Unauthenticated requests receive 403.
Trusted key groupRSA key pair managed via CDK / Secrets ManagerBFF holds the private key to sign cookies. CloudFront holds the public key to verify. Key group supports multiple active keys for zero-downtime rotation.
Cache behavior path/* (all objects in bucket)Single-purpose bucket; all objects are images
Cache policyCachingOptimized (or custom with long TTL)Images are immutable (new UUID on replacement). Signed cookies are not part of the cache key — all users in the same tenant share cached responses.
Viewer protocol policyHTTPS onlySecurity
Allowed HTTP methodsGET, HEADRead-only CDN
Price classPriceClass_100 (US, Canada, Europe)Cost optimization for initial deployment
Default TTL86400 (24 hours)Immutable objects; long caching is safe
Max TTL31536000 (1 year)With Cache-Control: immutable from S3

Cache invalidation is not required for normal operations. Object keys include a UUID, so image replacement creates a new key — the old key’s cache entry naturally expires. The entity’s imageUrl field is updated to point to the new key, so all subsequent requests go to the new object.

Invalidation may be needed for operational scenarios (e.g., removing harmful content). This is a manual or automated process outside the normal application flow.

SettingValue
Domain<partition>.<infrastructure>.assets.arda.cards (e.g., demo.alpha001.assets.arda.cards)
CertificateACM certificate for *.arda.cards (or specific subdomain)
Route 53CNAME/alias record pointing to CloudFront distribution

Role name convention: <partition>-image-upload-presigning-role

Follows the same pattern as the existing UploadPreSigningRole for CSV uploads in public-upload-bucket.ts.

PermissionResourceCondition
s3:PutObjectarn:aws:s3:::<bucket>/*aws:SecureTransport: true, s3:x-amz-server-side-encryption: AES256
s3:GetObjectarn:aws:s3:::<bucket>/*aws:SecureTransport: true

The Backend assumes this role via sts:AssumeRole to generate presigned POST forms. The role is separate from the pod’s service account role to maintain least-privilege separation. The bucket policy mirrors these conditions — PutObject requires HTTPS and SSE-S3 (AES256).

Existing role, extended with permissions for the new bucket:

PermissionResourcePurpose
s3:GetObjectarn:aws:s3:::<bucket>/*HEAD verification of uploaded objects
s3:ListBucketarn:aws:s3:::<bucket>Admin/cleanup operations
sts:AssumeRolePresigning role ARNAssume presigning role for POST form generation

Each uploaded image object carries S3 metadata. All metadata keys listed below are enforced via presigned POST policy conditions — the SPA must include them as form fields with the exact values specified in the policy, or S3 rejects the upload. The metadata elements are adapted from the existing CSV upload pattern (CsvS3BucketDirectAccess) where x-amz-meta-tenant-id and x-amz-meta-author are used as signed headers on the CSV presigned PUT-based workflow. This design carries the same elements forward, adapted to presigned POST policy conditions (TD-08).

Metadata KeyValuePolicy ConditionPurpose
x-amz-meta-tenant-idTenant UUIDeq (exact match)Audit trail, tenant identification. Validated post-upload via HeadObject (same as CSV upload matchingAttributes validation).
x-amz-meta-authorUser UUID (from JWT sub)eq (exact match)Audit trail. Identifies who uploaded the image.
x-amz-meta-arda-keyoperations/item/imageUrl/<uuid>.<ext>eq (exact match)Human-readable reference following the Arda-Key convention from the Static Asset Repository design.
Content-Typeimage/jpeg, image/png, etc.starts-with image/Only image content types accepted.
x-amz-server-side-encryptionAES256eq (exact match)Enforce SSE-S3 encryption (same requirement as CSV upload bucket policy).

Post-upload validation: After upload, the Backend calls HeadObject and validates that x-amz-meta-tenant-id matches the expected tenant (defense-in- depth). This mirrors CsvUploadService.processCsv which validates matchingAttributes before processing.

RulePrefixActionDays
Abort incomplete multipart uploads(all)AbortIncompleteMultipartUpload1
Expire orphaned staging objectsstaging/ (if staging prefix used)Expiration7

ConstructLocationPurpose
ImageAssetBucketinfrastructure/src/main/cdk/constructs/storage/image-asset-bucket.tsS3 bucket for persistent image assets. Parameterized from UploadBucket pattern but with no TTL expiration, versioning enabled, OAC integration.
ImageAssetCdninfrastructure/src/main/cdk/constructs/xgress/image-asset-cdn.tsCloudFront distribution with OAC origin to image asset bucket. Custom domain, HTTPS-only, caching optimized. Configured with trusted key groups for signed cookie validation (TD-11).
ImageUploadPresigningRoleinfrastructure/src/main/cdk/constructs/iam/image-upload-presigning-role.tsIAM role for presigned POST form generation. s3:PutObject + s3:GetObject on image asset bucket. Follows the same pattern as UploadPreSigningRole in public-upload-bucket.ts (HTTPS required, SSE-S3 required).
CloudFrontSigningKeyGroupinfrastructure/src/main/cdk/constructs/xgress/cloudfront-signing-key-group.tsRSA key pair and CloudFront key group for signed cookie validation. Public key uploaded to CloudFront; private key stored in Secrets Manager for BFF consumption. Supports multiple active keys for zero-downtime rotation.
StackLocationChange
PartitionBulkStoresStackinfrastructure/src/main/cdk/stacks/purpose/partition-bulk-stores.tsAdd ImageAssetBucket and ImageUploadPresigningRole. Export bucket ARN, name, and presigning role ARN via cross-stack exports.
PartitionIngressStack (or new stack)infrastructure/src/main/cdk/stacks/purpose/Add ImageAssetCdn distribution. Export CDN domain name.
FileChange
operations/src/main/cloudformation/pre-install.cfn.ymlImport new bucket ARN and presigning role ARN from infrastructure exports. Add IAM permissions (s3:GetObject, sts:AssumeRole) to pod service account role.
Export KeyValueConsumer
${Infrastructure}-${Purpose}-API-ImageAssetBucketArnBucket ARNOperations CloudFormation
${Infrastructure}-${Purpose}-API-ImageAssetBucketNameBucket nameOperations CloudFormation
${Infrastructure}-${Purpose}-API-ImagePresignRoleArnPresigning role ARNOperations CloudFormation
${Infrastructure}-${Purpose}-API-ImageCdnDomainCloudFront domainOperations (for CDN URL construction)
${Infrastructure}-${Purpose}-API-ImageCdnSigningKeyIdCloudFront key pair IDBFF (for cookie signing)
${Infrastructure}-${Purpose}-API-ImageCdnSigningKeySecretArnSecrets Manager ARN for private keyBFF (for cookie signing)

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