Phasing
Implementation phasing for the Item Image Upload feature. Each phase produces a verifiable artifact that can be tested independently before the next phase begins. For the sub-system structure and dependencies that inform this phasing, see design.md.
Design Principle
Section titled “Design Principle”Every phase ends with a concrete verification step — not “tests pass” in the abstract, but specific observable behaviors that confirm the phase is complete. This enables confident handoff between infrastructure, backend, and frontend tracks.
Dependency Structure
Section titled “Dependency Structure”Phases
Section titled “Phases”Phase 0: DNS Foundation [infrastructure]
Section titled “Phase 0: DNS Foundation [infrastructure]”Repository: infrastructure
Deliverables:
assets.arda.cardsRoute53 hosted zone inRootConfigurationStack<infra>.assets.arda.cardssubdomain zone with NS delegation inInfrastructureIngress*.<infra>.assets.arda.cardsACM wildcard certificate inInfrastructureIngress- Import assets zone and certificate in
ImportingStack ASSETS_DOMAIN_PREFIXandassetsDomain()inari-configuration.tsdeploy-root.shminimal root account deployment script
Entry criteria: None.
Verification:
cdk synthproduces valid CloudFormation templates for root and all infrastructure targets.deploy-root.shdeploysassets.arda.cardszone to root account.amm.shdeploys the infrastructure update (subdomain zone + cert) to dev environment.- DNS resolution:
dig NS alpha002.assets.arda.cardsreturns the subdomain zone’s name servers. - ACM certificate status is
ISSUED(DNS validation succeeded).
Parallelism: Starts immediately. No upstream dependencies. Must complete before Phase 1 CDN construct can use a custom domain.
For detailed specification, see 1-aws-infrastructure/specification.md section 2.4.
Phase 1: AWS Infrastructure [infrastructure]
Section titled “Phase 1: AWS Infrastructure [infrastructure]”Repository: infrastructure
Deliverables:
ImageAssetBucketCDK construct — persistent S3 bucket (no TTL, versioning, SSE-S3, CORS, lifecycle rules for incomplete multipart abort)ImageUploadPresigningRoleCDK construct — IAM role for presigned POST (s3:PutObject+s3:GetObjecton image bucket)ImageAssetCdnCDK construct — CloudFront distribution with OAC, custom domain (<partition>.<infra>.assets.arda.cards), trusted key groups for signed cookie validationCloudFrontSigningKeyGroupCDK construct — RSA key pair, public key in CloudFront trusted key group, private key in Secrets ManagerPartitionImageCdnStack— new stack for CDN + signing key group (PD-01)- Cross-stack exports:
${Infra}-${Purpose}-API-ImageAssetBucketArn${Infra}-${Purpose}-API-ImageAssetBucketName${Infra}-${Purpose}-API-ImagePresignRoleArn${Infra}-${Purpose}-API-ImageCdnDomain${Infra}-${Purpose}-API-ImageCdnSigningKeyId${Infra}-${Purpose}-API-ImageCdnSigningKeySecretArn
Entry criteria: Phase 0 deployed (assets zone and certificate available).
Verification:
cdk synthproduces valid CloudFormation templates.- Stack deploys to dev environment without errors.
- Presigning role can be assumed (
sts:AssumeRolesucceeds). - Test image uploaded to S3 and verified via
HeadObject. - CloudFront rejects unauthenticated requests with 403.
- Signed cookies (generated from the private key in Secrets Manager) grant CDN access to the uploaded image.
- Signed cookies scoped to Tenant A do not grant access to a different tenant’s path (tenant isolation at the CDN layer).
Steps 3-7 are automated by the verification script
infrastructure/tools/verify-image-cdn.ts.
Run it after cdk deploy:
npx ts-node tools/verify-image-cdn.ts \ --bucket <bucket-name> \ --cdn-domain <partition>.<infra>.assets.arda.cards \ --presign-role-arn arn:aws:iam::<account>:role/<presigning-role> \ --signing-key-id <CloudFront-key-pair-id> \ --signing-key-secret-arn arn:aws:secretsmanager:<region>:<account>:secret:<name> \ --tenant-id <test-tenant-uuid>Parallelism: Depends on Phase 0. Can start developing constructs against LocalStack before Phase 0 is deployed.
For detailed specification, see 1-aws-infrastructure/specification.md.
Phase 2a: Backend — common-module Library [common-module]
Section titled “Phase 2a: Backend — common-module Library [common-module]”Repository: common-module
Deliverables:
S3AssetService(cards.arda.common.lib.infra.storage) — presigned POST generation with full policy conditions (Content-Type, content-length-range, key, tenant-id, author, arda-key, SSE), HEAD verification, post-upload metadata validationAssetKeyGenerator(cards.arda.common.lib.infra.storage) — constructs tenant-scoped keys:<tenantId>/images/<uuid>.<ext>CdnUrlResolver(cards.arda.common.lib.infra.storage) — CDN URL construction from object keys; pattern validation for URL acceptance
Entry criteria: Phase 1 key format and CDN domain pattern defined (CDK synth sufficient; deployment not required).
Verification:
AssetKeyGeneratorTestpasses — key format, tenant prefix, UUID uniqueness, extension mapping.CdnUrlResolverTestpasses — URL construction, pattern validation, tenant isolation (rejects cross-tenant URLs).S3AssetServiceTestpasses (MockAWS/LocalStack) — presigned POST form generation with all policy conditions, HEAD verification, metadata validation.common-modulebuilds and publishes artifact to local Maven repository.
Parallelism: Can start during Phase 1 (develop against LocalStack).
Phase 2b: Backend — operations Endpoints [operations]
Section titled “Phase 2b: Backend — operations Endpoints [operations]”Repository: operations
Deliverables:
- Image upload routes in
ItemEndpoint(cards.arda.operations.reference.item.api.rest) —POST /v1/item/image-upload/request-upload-credentials(module-level, not entity-scoped — the upload occurs before entity persistence) ItemServicemodification —imageUrlvalidation step callingCdnUrlResolver.validate()directly (TD-14) andS3AssetService.headObject()before persist- Operations CloudFormation updated — import bucket ARN, bucket name, presigning role ARN, CDN domain from infrastructure exports
Entry criteria: Phase 2a artifact published.
Verification:
ImageUploadEndpointTestpasses (Harness + MockAWS) — auth, presigned generation, response format, correct pathPOST /v1/item/image-upload/request-upload-credentials.- Integration test:
POST /v1/item/image-upload/request-upload-credentialsreturnsuploadUrl,formFields,objectKey,cdnUrl. - Integration test:
PUT /v1/item/<itemEId>with CDN-patternimageUrlsucceeds after HEAD verification. - Integration test:
PUT /v1/item/<itemEId>with non-CDNimageUrlrejected (400). - Integration test:
PUT /v1/item/<itemEId>withimageUrl: nullclears the field. - Integration test:
PUT /v1/item/<itemEId>with unchanged non-CDNimageUrlsucceeds without validation (TD-15 grandfathering). operationsbuilds successfully.
Parallelism: Depends on 2a. Can run in parallel with Phase 1 deployment (using LocalStack for integration tests).
Phase 2c: Backend — Live Environment Verification [operations + infrastructure]
Section titled “Phase 2c: Backend — Live Environment Verification [operations + infrastructure]”Repositories: operations, infrastructure
Deliverables: Operations deployed to dev environment with real infrastructure imports. No new code — this phase is a deployment and integration gate.
Entry criteria: Phase 1 deployed + Phase 2b builds.
Verification:
- Operations pod starts successfully; can assume the presigning role.
POST /v1/item/image-upload/request-upload-credentials(curl or Bruno) returns valid presigned POST form fields.- Presigned POST form can upload an actual image to S3 (multipart form
submission to the
uploadUrlwith allformFields). PUT /v1/item/<itemEId>with the resulting CDN URL succeeds (Backend HEAD verification passes).- Image is served from CloudFront CDN at the expected URL.
Parallelism: Blocking gate. Must complete before BFF development can use real backend endpoints.
Phase 3a: BFF — Upload Proxy and URL Utilities [arda-frontend-app]
Section titled “Phase 3a: BFF — Upload Proxy and URL Utilities [arda-frontend-app]”Repository: arda-frontend-app (server-side)
Deliverables:
POST /api/item/image-upload/request-upload-credentials— proxy route (addsAuthorization,X-Tenant-Id,X-Author,X-Request-IDheaders, forwards to Backend)POST /api/storage/check-url— URL reachability check with SSRF protectionPOST /api/storage/fetch-url— image fetch proxy with SSRF protection (streams response, max 10 MB, 10s timeout)- Rate limiting middleware (in-memory per-instance, per-tenant, configurable default 30 req/min)
- Proxy configuration updates (
proxy.tsregistration)
Entry criteria: Phase 2 Backend endpoints available (dev environment or local).
Verification:
- BFF proxies
upload-urlrequest to Backend with correct auth and tenant headers; response passes through. check-urlperforms HEAD on external URL; returnsreachable,contentType,contentLength.check-urlreturns 422 for unreachable URLs.fetch-urlfetches and streams external image content back to caller.- SSRF protection:
check-urlandfetch-urlreject private IPs (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.0/8,169.254.0.0/16), localhost, non-HTTPS schemes, and managed storage URLs. - Rate limiting: exceeding 30 req/min/tenant returns 429.
- All routes return 401 without a valid session JWT.
Parallelism: Can start in parallel with Phase 2b (stub Backend responses for development). Full integration testing requires Phase 2c.
Phase 3b: BFF — CDN Cookie Signing [arda-frontend-app]
Section titled “Phase 3b: BFF — CDN Cookie Signing [arda-frontend-app]”Repository: arda-frontend-app (server-side)
Deliverables:
POST /api/storage/cdn-cookies— signed cookie issuance endpoint (extracts tenant from session, generates CloudFront custom policy, signs with private key, sets threeSet-Cookieheaders)cloudfront-signer.ts— cookie signing utility (loads private key from Secrets Manager or environment variable, generates policy/signature/key-pair-id cookies with configurable TTL)
Entry criteria: Phase 1 deployed (signing key pair in Secrets Manager, CloudFront trusted key group configured).
Verification:
CloudFrontSignerTestpasses — policy generation with correct tenant-scopedResourcefield (/<tenantId>/*),DateLessThanTTL, valid RSA signature.CdnCookiesRouteTestpasses — session validation, tenant extraction from authenticated context,Set-Cookieheaders present with correct attributes (Secure,HttpOnly,SameSite=Lax,Domain=.arda.cards).CdnCookiesTenantScopeTestpasses — cookie policyResourcematches active tenant; tenant switch produces new cookies scoped to new tenant.- End-to-end: cookies issued by BFF grant CDN access to the correct tenant’s images.
- Tenant isolation: cookies for Tenant A do not grant access to Tenant B’s images.
- Endpoint returns 401 without a valid session.
Parallelism: Independent of Phase 3a. Can start as soon as Phase 1 is deployed (needs signing key). Runs in parallel with Phases 2a, 2b, 3a.
Phase 4a: SPA — Component Library, Storybook-Driven [arda-frontend-app]
Section titled “Phase 4a: SPA — Component Library, Storybook-Driven [arda-frontend-app]”Repository: arda-frontend-app (client-side) — many components already
exist on the jmpicnic/item-image-upload-components branch
Deliverables: All 19 UI components per the UX Components specification:
| Tier | Components |
|---|---|
| Config | ImageFieldConfig, ITEM_IMAGE_CONFIG |
| Foundation | ImageDisplay, ImagePlaceholder, getCroppedImage |
| Molecules | ImageDropZone, ImagePreviewEditor, ImageComparisonLayout, CopyrightAcknowledgment |
| Organisms | ImageUploadDialog, ImageFormField, ImageCellDisplay, ImageCellEditor, ImageHoverPreview, ImageInspectorOverlay |
| Handlers (mock) | ImageUploadHandler, ImageReachabilityCheck (mock implementations for Storybook) |
Entry criteria: None.
Verification:
- All Storybook stories render correctly.
ImageUploadDialogstate machine transitions verified via play functions (EmptyImage → ProvidedImage → EditExisting → FailedValidation → Warn).- Unit tests pass for all components.
- Mock
ImageUploadHandlerandImageReachabilityCheckwork in stories. ImageFieldConfigparameterization verified (1:1 aspect ratio for Items, accepted formats, size limits).
Parallelism: Starts from day 1. No upstream dependencies. This is the longest individual work stream and benefits most from early start.
Phase 4b: SPA — CDN Cookie Lifecycle and Grid Integration [arda-frontend-app]
Section titled “Phase 4b: SPA — CDN Cookie Lifecycle and Grid Integration [arda-frontend-app]”Repository: arda-frontend-app
Deliverables:
- CDN cookie lifecycle manager:
- Request cookies at session start
- Proactive refresh timer at ~50% of TTL (~15 min for 30 min TTL)
- Immediate re-request on tenant switch
- 403 recovery: refresh cookies and retry (max 1 retry per image request)
- Grid integration:
ImageCellDisplaywired into item grid column definitions (replaces inline renderer)ImageCellEditorwired for inline edit (double-click / Enter)ImageHoverPreviewwired to grid thumbnails (~500ms hover delay)
- Loading states: shimmer while loading, placeholder + error badge on failure
Entry criteria: Phase 3b (cookie endpoint available) + Phase 4a (components built).
Verification:
- Images load in grid from CDN using signed cookies.
- Cookie proactive refresh fires on timer; images continue loading after original TTL would have expired.
- Tenant switch: images show shimmer → new cookies requested → images reload from new tenant’s CDN paths.
- 403 recovery: simulate expired cookie → auto-refresh → retry succeeds.
- Second 403 after retry: error-state placeholder with badge shown.
- Hover preview popover appears after ~500ms; dismisses on mouse-out.
- Hover preview not shown for error-state thumbnails.
- Double-click / Enter on image cell opens editor modal.
- Thumbnails do not cause layout reflow when loading asynchronously.
Parallelism: Depends on 3b + 4a.
Phase 4c: SPA — Upload Orchestration and Form Integration [arda-frontend-app]
Section titled “Phase 4c: SPA — Upload Orchestration and Form Integration [arda-frontend-app]”Repository: arda-frontend-app
Deliverables:
- Upload orchestration:
- Production
ImageUploadHandler— calls BFFimage-upload/request-upload-credentials, uploads to S3 via presigned POST, persists CDN URL on entity UploadingandUploadErrorsystem-level states inImageUploadDialog- Upload progress indicator (XHR/fetch
progressevent) - Single-upload-at-a-time enforcement (FR-041)
- Always-JPEG output (FR-042)
- Production
- External URL fetch-and-store:
- Production
ImageReachabilityCheck— direct HEAD, BFFcheck-urlfallback - Direct fetch or BFF
fetch-urlfallback for CORS-blocked URLs
- Production
- Form integration:
ImageFormFieldwired into Add Item form (optional image field)ImageFormFieldwired into Edit Item form (change / remove)- Remove image confirmation dialog →
imageUrl: nullpersistence
Entry criteria: Phase 3a (proxy endpoints) + Phase 3b (cookie endpoint) + Phase 4a (components).
Verification:
- File upload: file pick → preview → crop → confirm → upload to S3 → entity updated → CDN thumbnail appears in grid.
- Drag-and-drop: drag image file → same flow as file upload.
- Clipboard paste (blob): paste screenshot → preview → upload → entity updated.
- Clipboard paste (HTML with URL): paste → extract URL → fetch → preview → upload → entity updated.
- URL input: paste HTTPS URL → reachability check → fetch → preview → crop → confirm → upload → entity updated.
- URL input (CORS blocked): same flow but BFF
check-url/fetch-urlused as fallback. - Remove image: confirm dialog → entity cleared → placeholder shown.
- Add Item form: set image during creation → publish → image persists.
- Edit Item form: change image → save → reflected in grid.
- Edit Item form: remove image → save → placeholder in grid.
- Upload progress: progress indicator visible during upload.
- Upload error + retry: simulate network failure → error shown → retry succeeds without re-entering image.
- Copyright acknowledgment: upload blocked until checkbox accepted.
- Auto-compression: oversized image auto-compressed before upload.
Parallelism: Depends on 3a + 3b + 4a.
Phase 4d: SPA — End-to-End Integration [arda-frontend-app]
Section titled “Phase 4d: SPA — End-to-End Integration [arda-frontend-app]”Repository: arda-frontend-app
Deliverables: No new code. This phase is a verification gate confirming all 7 scenarios work end-to-end against the deployed environment.
Entry criteria: All previous phases deployed and working.
Verification (one item per scenario):
- S1 — Upload Image (Managed Path): File pick → preview → crop/zoom/rotate → copyright ack → confirm → upload → entity persisted → thumbnail in grid.
- S2 — External URL: Paste HTTPS URL → reachability → fetch → preview → crop → confirm → upload → entity persisted → thumbnail in grid.
- S3 — Remove Image: Click remove → confirm dialog → entity cleared → placeholder. Previous image retained in version history.
- S4 — View Image: Grid thumbnails load from CDN. Shimmer during load. Placeholder + error badge for unreachable images. Hover preview on ~500ms hover. No action icons in popover. Single click selects row.
- S5 — Grid Inline Edit: Double-click image cell → modal with full editor → upload → grid row refreshes with new thumbnail.
- S6 — Item Creation: Add Item → fill fields → set image → publish → image persists alongside entity.
- S7 — Item Edit: Edit Item → change image (comparison layout) → save. Edit Item → remove image → save → placeholder.
Parallelism: Blocking gate. Must pass before release.
Phase 5: API Tests [api-test]
Section titled “Phase 5: API Tests [api-test]”Repository: api-test
Deliverables:
- Bruno test collection for
POST /v1/item/image-upload/request-upload-credentials:- Happy path: valid request returns presigned form fields
- Auth failure: missing/invalid credentials → 401
- Invalid content type → 400
- Bruno test collection for entity update with image URL validation:
- CDN-pattern URL accepted (after HEAD verification)
- Non-CDN URL rejected → 400
- Null URL clears image
- End-to-end upload flow tests:
- Request presigned credentials → POST image to S3 → persist CDN URL on entity → verify entity has correct imageUrl
Entry criteria: Phases 1-3 deployed to dev environment.
Verification: All Bruno API tests pass against live dev environment.
Parallelism: Can run in parallel with Phase 4. Independent of SPA work.
Phase Summary
Section titled “Phase Summary”Recommended Sequence
Section titled “Recommended Sequence”The diagram below shows the low-risk implementation sequence. Phases flow left to right. Blue arrows are design dependencies — the downstream phase needs the API contract or artifact to write code. Red arrows are deploy gates — the upstream phase must be live and verified before the downstream phase can complete integration testing.
Three tracks run in parallel: the critical path (top row), the read-path track (middle), and the SPA component track (bottom). The tracks converge at Phase 4d (E2E integration).
Phase Sequence Table
Section titled “Phase Sequence Table”| # | Phase | Repository | Entry Criteria | Exit Criteria | Track |
|---|---|---|---|---|---|
| 0 | DNS Foundation | infrastructure | None | assets.arda.cards zone deployed to root; <infra>.assets.arda.cards subdomain zone + ACM cert deployed; DNS resolves; cert status ISSUED | Critical path |
| 1 | AWS Infrastructure | infrastructure | Phase 0 deployed | CDK deployed; verify-image-cdn.ts passes (presigning role, S3 upload, CDN 403 without cookies, CDN 200 with cookies, tenant isolation) | Critical path |
| 2a | common-module Library | common-module | Phase 1 key format and CDN domain defined (CDK synth) | S3AssetServiceTest, AssetKeyGeneratorTest, CdnUrlResolverTest pass; artifact published to local Maven | Critical path |
| 2b | operations Endpoints | operations | Phase 2a artifact published | ImageUploadEndpointTest passes; integration tests pass (presigned POST, CDN URL accepted, non-CDN rejected, null clears); operations builds | Critical path |
| 2c | Live Verification | operations + infrastructure | Phase 1 deployed + Phase 2b builds | Operations pod assumes presigning role; presigned POST uploads real image to S3; entity update with CDN URL succeeds; image served from CloudFront | Critical path |
| 3a | BFF Proxy & URL Utils | arda-frontend-app | Phase 2b endpoint contract defined | Proxy passes auth/tenant headers; check-url/fetch-url work; SSRF rejects private IPs; rate limiting returns 429; 401 without session | Critical path |
| 3b | BFF Cookie Signing | arda-frontend-app | Phase 1 deployed (signing key in Secrets Manager) | CloudFrontSignerTest passes; cookies grant CDN access; wrong-tenant cookies get 403; cookie attributes correct (Secure, HttpOnly, SameSite=Lax) | Read-path |
| 4a | SPA Components | arda-frontend-app | None | 19 components built; Storybook stories render; unit tests pass; mock handlers work | Components |
| 4b | Grid + Cookies | arda-frontend-app | Phase 3b + Phase 4a | Thumbnails load from CDN; proactive refresh works; tenant switch re-scopes; 403 recovery works; hover preview on ~500ms; double-click opens editor | Read-path |
| 4c | Upload + Forms | arda-frontend-app | Phase 3a + Phase 3b + Phase 4a | All input methods work (file, drag-drop, clipboard blob, clipboard HTML, URL, CORS fallback); remove works; Add Item and Edit Item forms functional; progress indicator; copyright gate | Critical path |
| 4d | E2E Integration | arda-frontend-app | All prior phases | Scenarios S1-S7 verified end-to-end against deployed environment | Convergence |
| 5 | API Tests | api-test | Phases 1-3 deployed | Bruno test collections pass against live dev environment | Independent |
Parallel Tracks
Section titled “Parallel Tracks”The three tracks exploit the distinction between design dependencies and deploy gates to maximize concurrency:
Critical path (longest sequential chain):
0 → 1 → 2a → 2b → 2c → 3a → 4c → 4d.
This is the chain where every phase must complete before the next can start.
Phase 0 establishes the DNS foundation; Phase 1 builds on it with partition
resources. Phase 2c is a deploy gate (red) that validates the infra+backend
integration before BFF work begins against real endpoints.
Read-path track (0 → 1 → 3b → 4b → 4d):
Phase 3b only needs the signing key from Phase 1 — it has no dependency on the
Backend at all. This means cookie signing and grid integration can be developed
and tested while the Backend phases (2a, 2b, 2c) are still in progress. The
read-path track converges with the critical path at Phase 4d.
Component track (4a → 4b, 4c):
Phase 4a has zero dependencies and starts from day 1. It is the longest
individual work stream (19 components + Storybook stories). Its output feeds
both Phase 4b (grid integration) and Phase 4c (upload orchestration). Starting
early ensures components are ready before either integration phase needs them.
Phase 5 (API tests) is independent of all SPA work. It starts as soon as Phases 1-3 are deployed and runs in parallel with Phase 4.
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved