Skip to content

System Design

Structural design for the Item Image Upload feature. This document defines the sub-systems, their functional responsibilities, the information each sub-system owns, and the dependencies between them. It provides the technical context for the requirements, scenarios, and individual sub-system specifications.

Design decisions referenced here (TD-01 through TD-14) are recorded in the project decision log.

The feature is decomposed into five sub-systems. Each sub-system has a single responsibility domain and communicates with others through well-defined interfaces.

PlantUML diagram


SPA (React) — Interaction and Orchestration

Section titled “SPA (React) — Interaction and Orchestration”

The SPA is the primary interaction point. It owns all user-facing behavior and orchestrates the multi-step upload workflow.

ResponsibilityDescriptionDesign Decision
Input detectionClassify user input (file, drag-drop, clipboard blob, clipboard HTML, URL text, data: URI, camera) and route through the appropriate processing path. All paths converge to managed upload.TD-01
Image editingCrop, zoom, rotate, pan, reset. Locked aspect ratio per entity type. Always produces JPEG output.FR-042
Upload orchestrationRequest presigned credentials from BFF → upload Blob to S3 → persist CDN URL on entity via BFF. File bytes never touch BFF or Backend.TD-06
CDN cookie lifecycleRequest signed cookies at session start, proactively refresh at ~50% TTL, re-request on tenant switch, retry on 403.TD-12
DisplayRender thumbnails from CDN, shimmer/placeholder/error states, hover preview, grid inline edit.
ValidationFormat, size, minimum dimensions, URL scheme, copyright acknowledgment. All errors in plain language.TD-04

Information owned: Form state (in-progress image edits), upload state machine (Uploading, UploadError), cookie refresh timer, ImageFieldConfig per entity type.

Specification: spa-specification.md


Section titled “BFF (Next.js) — Auth Proxy, URL Utilities, Cookie Signing”

The BFF is a stateless proxy that enriches requests with authentication and tenant context. It also provides two capabilities the SPA cannot perform directly: SSRF-protected external URL fetching and CloudFront cookie signing.

ResponsibilityDescriptionDesign Decision
Auth proxyForward presigned credential and entity update requests to Backend with Authorization, X-Tenant-Id, X-Author, X-Request-ID headers.TD-03
URL reachabilityHEAD/GET external URLs on behalf of the SPA when CORS blocks direct access. SSRF protection (private IP rejection, HTTPS-only, managed storage rejection). Rate-limited per tenant.TD-02
URL fetchStream external image content to SPA when CORS blocks direct fetch. Same SSRF protection.TD-02
Cookie signingGenerate CloudFront signed cookies scoped to the active tenant’s key prefix. Load signing private key from Secrets Manager. Set cookies on .arda.cards domain.TD-11, TD-12

Information owned: Rate limiter state (in-memory per instance). No image data, no upload state, no entity state.

Key constraint: The BFF does not depend on AWS Storage resources. It never handles file bytes (TD-06), never checks URLs on managed storage (TD-02), and never generates presigned credentials (TD-03).

Specification: bff-specification.md


Backend (Operations + common-module) — Credentials, Validation, Persistence

Section titled “Backend (Operations + common-module) — Credentials, Validation, Persistence”

The Backend is the authority for presigned credential generation, URL validation, and entity persistence. It enforces that all persisted image URLs originate from managed storage.

ResponsibilityDescriptionDesign Decision
Presigned credential generationAssume IAM presigning role, generate presigned POST form with policy conditions (Content-Type, content-length-range, key, tenant-id, author, arda-key, SSE). Return form fields, upload URL, object key, and CDN URL.TD-03, TD-08
CDN URL validationValidate that imageUrl values match the expected CDN host and key pattern. Validate tenant prefix matches the requesting tenant. Reject all non-managed URLs. CdnUrlResolver in common-module (TD-14).TD-05
Upload verificationHeadObject on S3 to verify the uploaded object exists and metadata (x-amz-meta-tenant-id) matches. Single call satisfies both checks.
Entity persistencePersist imageUrl on the entity via bitemporal Universe. Accept null to clear. Retain previous references in version history.
Key generationAssetKeyGenerator constructs tenant-scoped keys: <tenantId>/images/<uuid>.<ext>. UUID generated server-side.

Information owned: Entity state (Item with imageUrl), S3 key convention, CDN URL pattern. Presigned credentials are stateless — no server-side tracking.

Modules:

  • common-module: S3AssetService, AssetKeyGenerator, CdnUrlResolver
  • operations: ImageUploadEndpoint, modified ItemService

Specification: backend-specification.md


Storage (AWS) — Persistence, Delivery, Access Control

Section titled “Storage (AWS) — Persistence, Delivery, Access Control”

Storage is a black box in scenario diagrams (TD-07). This sub-system owns image persistence, CDN delivery, and cryptographic access control. The specific AWS resources are detailed in the aws-specification.md.

ResponsibilityDescriptionDesign Decision
Image persistenceS3 bucket: persistent (no TTL), versioned, SSE-S3 encrypted, tenant-partitioned keys.
CDN deliveryCloudFront distribution with OAC. Serves images at edge. Cache-friendly (immutable keys, new UUID per upload).TD-06, TD-07
Upload authorizationPresigned POST policy conditions enforced server-side by S3. Content-Type, content-length-range, key, metadata — all validated on upload.TD-08
Read authorizationCloudFront signed cookies required for all requests. Cookie policy scoped to /<tenantId>/*. Unauthenticated requests return 403.TD-11
Key managementRSA key pair: public key in CloudFront trusted key group, private key in Secrets Manager. Supports zero-downtime rotation.TD-11
Presigning roleIAM role with s3:PutObject + s3:GetObject on the image bucket. Assumed by Backend via sts:AssumeRole.

Information owned: Image bytes, S3 object metadata (tenant-id, author, arda-key), CloudFront cache, signing key pair.

Specification: aws-specification.md


The feature has two distinct data flows: the write path (upload) and the read path (display). They share the CDN URL as the interface contract but are otherwise independent.

Image bytes flow from User → SPA → Storage. Metadata flows through SPA → BFF → Backend → Storage. The two paths are intentionally separated (TD-06): the BFF and Backend never handle file bytes.

User provides image
→ SPA detects, validates, edits (produces JPEG Blob)
→ SPA requests presigned credentials (via BFF → Backend)
← Backend returns {uploadUrl, formFields, objectKey, cdnUrl}
→ SPA uploads Blob directly to S3 (presigned POST)
→ SPA persists cdnUrl on entity (via BFF → Backend)
→ Backend validates URL pattern, verifies object exists (HEAD)
→ Backend persists entity

Image bytes flow from Storage → SPA (via CDN). Authentication is handled entirely by CloudFront signed cookies — no Backend involvement in the read path (TD-06).

SPA holds valid signed cookies (issued by BFF, scoped to tenant)
→ Browser requests <img src="CDN URL">
→ Browser sends cookies automatically (same-site .arda.cards)
→ CloudFront validates cookie signature and tenant prefix
→ CloudFront serves image from cache (or fetches from S3 origin)

Cookie lifecycle: issued at session start, proactively refreshed at ~50% TTL (~15 min for 30 min default), re-issued immediately on tenant switch. See cdn-access-control.md for the full security analysis.


The dependency diagram below shows what each sub-system needs from others. Dependencies are structural (required for the sub-system to function), not temporal (for implementation sequencing, see phasing.md).

PlantUML diagram

Key structural properties:

  • SPA → Storage is direct for both write (presigned POST) and read (CDN). The BFF and Backend are not in the data path for image bytes (TD-06).
  • BFF → Storage is minimal: only Secrets Manager access for the signing private key. The BFF never reads from or writes to S3.
  • BFF → Backend is one-directional proxy: the BFF adds auth headers and forwards. It makes no business decisions.
  • Backend → Storage is SDK-based: presigned POST generation (via assumed IAM role) and HEAD verification (via pod service account).
  • BFF ↛ External URLs is a fallback path, not a primary path. Most image inputs (file, clipboard, camera) never involve external URLs.

The S3 object key is the primary interface contract shared across sub-systems. All sub-systems must agree on its format.

<tenantId>/images/<uuid>.<ext>
SegmentOwnerPurpose
<tenantId>Backend (from auth context)Tenant isolation. CloudFront cookie policy scoped to /<tenantId>/*.
imagesConvention (fixed literal)Feature namespace. Distinguishes from future asset types.
<uuid>Backend (AssetKeyGenerator)Uniqueness and immutability. New UUID per upload (including replacements).
<ext>Backend (from contentType)Content-type hint. image/jpegjpg, etc.

The CDN URL is constructed by prepending the CloudFront domain: https://<partition>.<infra>.assets.arda.cards/<tenantId>/images/<uuid>.<ext>

Constraints:

  • The Backend generates keys server-side. The SPA never constructs keys.
  • The tenant ID segment must match the authenticated tenant.
  • UUID is cryptographically random (java.util.UUID.randomUUID()).

Full specification: aws-specification.md — Object Key Structure


The presigned POST form is the interface contract between Backend (which generates it), SPA (which submits it), and S3 (which validates it). It is the mechanism that allows the SPA to upload directly to S3 without the Backend handling file bytes (TD-06, TD-08).

PartyRole
BackendGenerates presigned POST form with policy conditions. Returns {uploadUrl, formFields, objectKey, cdnUrl}.
SPASubmits all formFields as multipart form data alongside the file (file must be last field).
S3Validates policy conditions server-side. Rejects upload if any condition fails.

Policy conditions (enforced by S3):

ConditionEnforcement
key = exact matchUpload targets only the specified key
Content-Type starts-with image/Only image content types
content-length-range [1, maxSize]Server-side file size enforcement
x-amz-meta-tenant-id = exactTenant baked into policy
x-amz-meta-author = exactAuthor baked into policy
x-amz-server-side-encryption = AES256Encryption enforced
Expiry = 15 minutesAligned with existing uploadSignatureDuration

Full specification: aws-specification.md — Presigned POST


Product images are sensitive (A-001). CDN access is restricted via CloudFront signed cookies scoped to the active tenant’s key prefix.

AspectDetail
MechanismCloudFront signed cookies (RSA custom policy)
ScopeResource: https://<partition>.<infra>.assets.arda.cards/<tenantId>/*
TTL30 minutes (configurable), proactive refresh at ~50%
IssuerBFF cdn-cookies endpoint (extracts tenant from session, never from client parameter)
Cookie attributesDomain=.arda.cards; Secure; HttpOnly; SameSite=Lax
Tenant switchImmediate re-issuance; old cookies overwritten
403 recoverySPA refreshes cookies and retries (max 1 retry per request)

Full specification: cdn-access-control.md


Each sub-system has a dedicated specification document with four sections: Requirements, Interfaces, State, and Modules.

Sub-SystemSpecificationKey Interfaces
SPAspa-specification.mdUser events → BFF API → Storage (presigned POST, CDN GET)
BFFbff-specification.mdSPA API routes → Backend REST → External URLs → Secrets Manager
Backendbackend-specification.mdBFF REST → S3 (presign, HEAD) → Entity persistence
Storageaws-specification.mdSPA (presigned POST, CDN GET) ← Backend (presign, HEAD) ← BFF (Secrets Manager)

These specifications build on the structural context defined in this document. The scenarios show how the sub-systems interact at runtime for each use case. The phasing shows the recommended implementation sequence.


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