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.
Sub-Systems
Section titled “Sub-Systems”The feature is decomposed into five sub-systems. Each sub-system has a single responsibility domain and communicates with others through well-defined interfaces.
Sub-System Responsibilities
Section titled “Sub-System Responsibilities”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.
| Responsibility | Description | Design Decision |
|---|---|---|
| Input detection | Classify 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 editing | Crop, zoom, rotate, pan, reset. Locked aspect ratio per entity type. Always produces JPEG output. | FR-042 |
| Upload orchestration | Request 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 lifecycle | Request signed cookies at session start, proactively refresh at ~50% TTL, re-request on tenant switch, retry on 403. | TD-12 |
| Display | Render thumbnails from CDN, shimmer/placeholder/error states, hover preview, grid inline edit. | — |
| Validation | Format, 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
BFF (Next.js) — Auth Proxy, URL Utilities, Cookie Signing
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.
| Responsibility | Description | Design Decision |
|---|---|---|
| Auth proxy | Forward presigned credential and entity update requests to Backend with Authorization, X-Tenant-Id, X-Author, X-Request-ID headers. | TD-03 |
| URL reachability | HEAD/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 fetch | Stream external image content to SPA when CORS blocks direct fetch. Same SSRF protection. | TD-02 |
| Cookie signing | Generate 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.
| Responsibility | Description | Design Decision |
|---|---|---|
| Presigned credential generation | Assume 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 validation | Validate 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 verification | HeadObject on S3 to verify the uploaded object exists and metadata (x-amz-meta-tenant-id) matches. Single call satisfies both checks. | — |
| Entity persistence | Persist imageUrl on the entity via bitemporal Universe. Accept null to clear. Retain previous references in version history. | — |
| Key generation | AssetKeyGenerator 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,CdnUrlResolveroperations:ImageUploadEndpoint, modifiedItemService
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.
| Responsibility | Description | Design Decision |
|---|---|---|
| Image persistence | S3 bucket: persistent (no TTL), versioned, SSE-S3 encrypted, tenant-partitioned keys. | — |
| CDN delivery | CloudFront distribution with OAC. Serves images at edge. Cache-friendly (immutable keys, new UUID per upload). | TD-06, TD-07 |
| Upload authorization | Presigned POST policy conditions enforced server-side by S3. Content-Type, content-length-range, key, metadata — all validated on upload. | TD-08 |
| Read authorization | CloudFront signed cookies required for all requests. Cookie policy scoped to /<tenantId>/*. Unauthenticated requests return 403. | TD-11 |
| Key management | RSA key pair: public key in CloudFront trusted key group, private key in Secrets Manager. Supports zero-downtime rotation. | TD-11 |
| Presigning role | IAM 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
Information Flow
Section titled “Information Flow”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.
Write Path (Upload)
Section titled “Write Path (Upload)”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 entityRead Path (Display)
Section titled “Read Path (Display)”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.
Sub-System Dependencies
Section titled “Sub-System Dependencies”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).
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.
Object Key Convention
Section titled “Object Key Convention”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>| Segment | Owner | Purpose |
|---|---|---|
<tenantId> | Backend (from auth context) | Tenant isolation. CloudFront cookie policy scoped to /<tenantId>/*. |
images | Convention (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/jpeg → jpg, 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
Presigned POST Contract
Section titled “Presigned POST Contract”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).
| Party | Role |
|---|---|
| Backend | Generates presigned POST form with policy conditions. Returns {uploadUrl, formFields, objectKey, cdnUrl}. |
| SPA | Submits all formFields as multipart form data alongside the file (file must be last field). |
| S3 | Validates policy conditions server-side. Rejects upload if any condition fails. |
Policy conditions (enforced by S3):
| Condition | Enforcement |
|---|---|
key = exact match | Upload 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 = exact | Tenant baked into policy |
x-amz-meta-author = exact | Author baked into policy |
x-amz-server-side-encryption = AES256 | Encryption enforced |
| Expiry = 15 minutes | Aligned with existing uploadSignatureDuration |
Full specification: aws-specification.md — Presigned POST
CDN Access Control
Section titled “CDN Access Control”Product images are sensitive (A-001). CDN access is restricted via CloudFront signed cookies scoped to the active tenant’s key prefix.
| Aspect | Detail |
|---|---|
| Mechanism | CloudFront signed cookies (RSA custom policy) |
| Scope | Resource: https://<partition>.<infra>.assets.arda.cards/<tenantId>/* |
| TTL | 30 minutes (configurable), proactive refresh at ~50% |
| Issuer | BFF cdn-cookies endpoint (extracts tenant from session, never from client parameter) |
| Cookie attributes | Domain=.arda.cards; Secure; HttpOnly; SameSite=Lax |
| Tenant switch | Immediate re-issuance; old cookies overwritten |
| 403 recovery | SPA refreshes cookies and retries (max 1 retry per request) |
Full specification: cdn-access-control.md
Sub-System Specifications
Section titled “Sub-System Specifications”Each sub-system has a dedicated specification document with four sections: Requirements, Interfaces, State, and Modules.
| Sub-System | Specification | Key Interfaces |
|---|---|---|
| SPA | spa-specification.md | User events → BFF API → Storage (presigned POST, CDN GET) |
| BFF | bff-specification.md | SPA API routes → Backend REST → External URLs → Secrets Manager |
| Backend | backend-specification.md | BFF REST → S3 (presign, HEAD) → Entity persistence |
| Storage | aws-specification.md | SPA (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
Copyright: © Arda Systems 2025-2026, All rights reserved