Skip to content

Static Asset Repository

The Static Asset Repository provides per-tenant storage and access control for customer assets (images, videos, documents, etc.). It is designed as a microservice encapsulating a single bounded context — loosely coupled from other components for independent development, deployment, and scaling.

Three layers compose the repository:

LayerTechnologyResponsibility
Access ControlAPI Gateway + CognitoJWT authentication; redirect unauthenticated requests to login
API LayerAWS LambdaTenant namespace partitioning, pre-signed URL generation
Storage BackendS3 (single bucket, or multiple if needed)Object storage, versioning, encryption

Only the Lambda function has direct access to the S3 bucket. The bucket is invisible to API consumers.

KeyFormatPurpose
Arda-Key{owning-module}/{entity-type}/{property-name}/{asset-name}.{ext}Human-readable identifier specified by the caller
System-Key{uuid}/{basename from Arda-Key}Unique identifier generated at creation; prevents name clashes
Storage-Key{tenantId}/{System-Key}Actual S3 object key; partitions storage by tenant

The Arda-Key is stored as S3 object metadata (arda-key). The API maps dynamically between System-Key and Storage-Key.

Pre-Signed URL — time-limited (initially 5-minute expiry), generated per request, provides direct S3 read or write access. Used for the actual upload/download to avoid routing large payloads through Lambda.

Persistent URL — stable URL of the form https://{api-gateway-host}/assets/{systemKey}. Stored in entity payloads to reference the asset long-term. Resolves to a pre-signed URL at read time via a 302 redirect.

MethodPathDescription
POST/assetsRegister an asset; returns Persistent URL + pre-signed PUT URL
GET/assets/{systemKey}302 redirect to pre-signed GET URL
HEAD/assets/{systemKey}Asset headers (content-type, content-length, checksum)
GET/versions/{systemKey}List all versions of the asset

All endpoints require a valid JWT token. The API Gateway handles authentication; the Lambda handles authorization (currently a no-op, planned for future implementation).

  1. Client (SPA via BFF) calls POST /assets with ardaKey, contentType, optional contentLength and checksum
  2. Lambda generates System-Key and Storage-Key, creates a pre-signed PUT URL, returns (persistentUrl, presignedUrl)
  3. Client uploads file bytes directly to S3 using the pre-signed PUT URL
  4. Client persists the Persistent URL in the owning entity via the relevant Component’s API
  5. The Component calls HEAD {persistentUrl} to verify the upload completed before persisting
  1. Client retrieves the Persistent URL from the entity via the Component API
  2. Client calls GET {persistentUrl} through API Gateway
  3. Lambda resolves System-Key to Storage-Key, generates a pre-signed GET URL
  4. API returns 302 redirect to the pre-signed GET URL
  5. Client fetches the asset directly from S3

S3 Bucket:

  • Block all public access (on by default)
  • Server-side encryption: SSE-S3 (Amazon managed keys)
  • Bucket Versioning: enabled (audit trail and rollback)
  • Removal Policy: RETAIN (bucket survives CDK stack deletion)
  • autoDeleteObjects: false (objects preserved on bucket deletion)
  • Object ownership: bucket owner (ACLs disabled)
  • Default tags: Infrastructure, Partition, Component

S3 Object Metadata per Asset:

  • arda-user — JWT sub claim of the creating user
  • arda-key — Arda-Key of the asset (for logging and debugging)

IAM Policy for Lambda (least privilege):

  • s3:PutObject, s3:GetObject, s3:GetObjectVersion, s3:ListBucketVersions
  • No other IAM policies grant access to the bucket; bucket policy explicitly denies all other access
EventExpected BehaviorOperator Action
CreateCloudFormation creates and configures the bucketVerify tags and baseline controls
Update (no replacement)In-place configuration updateValidate no permission regression
Update (replacement required)New bucket created; old bucket retainedPlan data migration before cutover
Stack deletionStack deleted; bucket and versions retainedRecord retained bucket in inventory
Final decommissionNo automatic deletionFollow decommission runbook

Decommission runbook:

  1. Obtain legal/compliance approval
  2. Archive/export required data to long-term storage if mandated
  3. Remove all objects, versions, and delete markers
  4. Delete the bucket
  5. Close the decommission ticket and remove from retained-resources inventory

The user-facing behavioral contracts that drive this infrastructure are defined in the product use case specifications. See Use Cases Analysis (not yet published) for the full traceability between use cases and design decisions.

Use CaseRelevance to FileStore
GEN::MEDIA::0001Set Entity Image — defines the user-facing upload and URL input flows that the Write Flow implements
GEN::MEDIA::0002Remove Entity Image — clears the entity reference; superseded assets are retained for historical display (archival/purging is out of scope)