Skip to content

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.

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.

PlantUML diagram


Repository: infrastructure

Deliverables:

  • assets.arda.cards Route53 hosted zone in RootConfigurationStack
  • <infra>.assets.arda.cards subdomain zone with NS delegation in InfrastructureIngress
  • *.<infra>.assets.arda.cards ACM wildcard certificate in InfrastructureIngress
  • Import assets zone and certificate in ImportingStack
  • ASSETS_DOMAIN_PREFIX and assetsDomain() in ari-configuration.ts
  • deploy-root.sh minimal root account deployment script

Entry criteria: None.

Verification:

  1. cdk synth produces valid CloudFormation templates for root and all infrastructure targets.
  2. deploy-root.sh deploys assets.arda.cards zone to root account.
  3. amm.sh deploys the infrastructure update (subdomain zone + cert) to dev environment.
  4. DNS resolution: dig NS alpha002.assets.arda.cards returns the subdomain zone’s name servers.
  5. 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:

  • ImageAssetBucket CDK construct — persistent S3 bucket (no TTL, versioning, SSE-S3, CORS, lifecycle rules for incomplete multipart abort)
  • ImageUploadPresigningRole CDK construct — IAM role for presigned POST (s3:PutObject + s3:GetObject on image bucket)
  • ImageAssetCdn CDK construct — CloudFront distribution with OAC, custom domain (<partition>.<infra>.assets.arda.cards), trusted key groups for signed cookie validation
  • CloudFrontSigningKeyGroup CDK construct — RSA key pair, public key in CloudFront trusted key group, private key in Secrets Manager
  • PartitionImageCdnStack — 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:

  1. cdk synth produces valid CloudFormation templates.
  2. Stack deploys to dev environment without errors.
  3. Presigning role can be assumed (sts:AssumeRole succeeds).
  4. Test image uploaded to S3 and verified via HeadObject.
  5. CloudFront rejects unauthenticated requests with 403.
  6. Signed cookies (generated from the private key in Secrets Manager) grant CDN access to the uploaded image.
  7. 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:

Terminal window
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 validation
  • AssetKeyGenerator (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:

  1. AssetKeyGeneratorTest passes — key format, tenant prefix, UUID uniqueness, extension mapping.
  2. CdnUrlResolverTest passes — URL construction, pattern validation, tenant isolation (rejects cross-tenant URLs).
  3. S3AssetServiceTest passes (MockAWS/LocalStack) — presigned POST form generation with all policy conditions, HEAD verification, metadata validation.
  4. common-module builds 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)
  • ItemService modification — imageUrl validation step calling CdnUrlResolver.validate() directly (TD-14) and S3AssetService.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:

  1. ImageUploadEndpointTest passes (Harness + MockAWS) — auth, presigned generation, response format, correct path POST /v1/item/image-upload/request-upload-credentials.
  2. Integration test: POST /v1/item/image-upload/request-upload-credentials returns uploadUrl, formFields, objectKey, cdnUrl.
  3. Integration test: PUT /v1/item/<itemEId> with CDN-pattern imageUrl succeeds after HEAD verification.
  4. Integration test: PUT /v1/item/<itemEId> with non-CDN imageUrl rejected (400).
  5. Integration test: PUT /v1/item/<itemEId> with imageUrl: null clears the field.
  6. Integration test: PUT /v1/item/<itemEId> with unchanged non-CDN imageUrl succeeds without validation (TD-15 grandfathering).
  7. operations builds 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:

  1. Operations pod starts successfully; can assume the presigning role.
  2. POST /v1/item/image-upload/request-upload-credentials (curl or Bruno) returns valid presigned POST form fields.
  3. Presigned POST form can upload an actual image to S3 (multipart form submission to the uploadUrl with all formFields).
  4. PUT /v1/item/<itemEId> with the resulting CDN URL succeeds (Backend HEAD verification passes).
  5. 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 (adds Authorization, X-Tenant-Id, X-Author, X-Request-ID headers, forwards to Backend)
  • POST /api/storage/check-url — URL reachability check with SSRF protection
  • POST /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.ts registration)

Entry criteria: Phase 2 Backend endpoints available (dev environment or local).

Verification:

  1. BFF proxies upload-url request to Backend with correct auth and tenant headers; response passes through.
  2. check-url performs HEAD on external URL; returns reachable, contentType, contentLength.
  3. check-url returns 422 for unreachable URLs.
  4. fetch-url fetches and streams external image content back to caller.
  5. SSRF protection: check-url and fetch-url reject 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.
  6. Rate limiting: exceeding 30 req/min/tenant returns 429.
  7. 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.


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 three Set-Cookie headers)
  • 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:

  1. CloudFrontSignerTest passes — policy generation with correct tenant-scoped Resource field (/<tenantId>/*), DateLessThan TTL, valid RSA signature.
  2. CdnCookiesRouteTest passes — session validation, tenant extraction from authenticated context, Set-Cookie headers present with correct attributes (Secure, HttpOnly, SameSite=Lax, Domain=.arda.cards).
  3. CdnCookiesTenantScopeTest passes — cookie policy Resource matches active tenant; tenant switch produces new cookies scoped to new tenant.
  4. End-to-end: cookies issued by BFF grant CDN access to the correct tenant’s images.
  5. Tenant isolation: cookies for Tenant A do not grant access to Tenant B’s images.
  6. 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:

TierComponents
ConfigImageFieldConfig, ITEM_IMAGE_CONFIG
FoundationImageDisplay, ImagePlaceholder, getCroppedImage
MoleculesImageDropZone, ImagePreviewEditor, ImageComparisonLayout, CopyrightAcknowledgment
OrganismsImageUploadDialog, ImageFormField, ImageCellDisplay, ImageCellEditor, ImageHoverPreview, ImageInspectorOverlay
Handlers (mock)ImageUploadHandler, ImageReachabilityCheck (mock implementations for Storybook)

Entry criteria: None.

Verification:

  1. All Storybook stories render correctly.
  2. ImageUploadDialog state machine transitions verified via play functions (EmptyImage → ProvidedImage → EditExisting → FailedValidation → Warn).
  3. Unit tests pass for all components.
  4. Mock ImageUploadHandler and ImageReachabilityCheck work in stories.
  5. ImageFieldConfig parameterization 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.


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:
    • ImageCellDisplay wired into item grid column definitions (replaces inline renderer)
    • ImageCellEditor wired for inline edit (double-click / Enter)
    • ImageHoverPreview wired 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:

  1. Images load in grid from CDN using signed cookies.
  2. Cookie proactive refresh fires on timer; images continue loading after original TTL would have expired.
  3. Tenant switch: images show shimmer → new cookies requested → images reload from new tenant’s CDN paths.
  4. 403 recovery: simulate expired cookie → auto-refresh → retry succeeds.
  5. Second 403 after retry: error-state placeholder with badge shown.
  6. Hover preview popover appears after ~500ms; dismisses on mouse-out.
  7. Hover preview not shown for error-state thumbnails.
  8. Double-click / Enter on image cell opens editor modal.
  9. 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 BFF image-upload/request-upload-credentials, uploads to S3 via presigned POST, persists CDN URL on entity
    • Uploading and UploadError system-level states in ImageUploadDialog
    • Upload progress indicator (XHR/fetch progress event)
    • Single-upload-at-a-time enforcement (FR-041)
    • Always-JPEG output (FR-042)
  • External URL fetch-and-store:
    • Production ImageReachabilityCheck — direct HEAD, BFF check-url fallback
    • Direct fetch or BFF fetch-url fallback for CORS-blocked URLs
  • Form integration:
    • ImageFormField wired into Add Item form (optional image field)
    • ImageFormField wired into Edit Item form (change / remove)
    • Remove image confirmation dialog → imageUrl: null persistence

Entry criteria: Phase 3a (proxy endpoints) + Phase 3b (cookie endpoint) + Phase 4a (components).

Verification:

  1. File upload: file pick → preview → crop → confirm → upload to S3 → entity updated → CDN thumbnail appears in grid.
  2. Drag-and-drop: drag image file → same flow as file upload.
  3. Clipboard paste (blob): paste screenshot → preview → upload → entity updated.
  4. Clipboard paste (HTML with URL): paste → extract URL → fetch → preview → upload → entity updated.
  5. URL input: paste HTTPS URL → reachability check → fetch → preview → crop → confirm → upload → entity updated.
  6. URL input (CORS blocked): same flow but BFF check-url/fetch-url used as fallback.
  7. Remove image: confirm dialog → entity cleared → placeholder shown.
  8. Add Item form: set image during creation → publish → image persists.
  9. Edit Item form: change image → save → reflected in grid.
  10. Edit Item form: remove image → save → placeholder in grid.
  11. Upload progress: progress indicator visible during upload.
  12. Upload error + retry: simulate network failure → error shown → retry succeeds without re-entering image.
  13. Copyright acknowledgment: upload blocked until checkbox accepted.
  14. 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):

  1. S1 — Upload Image (Managed Path): File pick → preview → crop/zoom/rotate → copyright ack → confirm → upload → entity persisted → thumbnail in grid.
  2. S2 — External URL: Paste HTTPS URL → reachability → fetch → preview → crop → confirm → upload → entity persisted → thumbnail in grid.
  3. S3 — Remove Image: Click remove → confirm dialog → entity cleared → placeholder. Previous image retained in version history.
  4. 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.
  5. S5 — Grid Inline Edit: Double-click image cell → modal with full editor → upload → grid row refreshes with new thumbnail.
  6. S6 — Item Creation: Add Item → fill fields → set image → publish → image persists alongside entity.
  7. S7 — Item Edit: Edit Item → change image (comparison layout) → save. Edit Item → remove image → save → placeholder.

Parallelism: Blocking gate. Must pass before release.


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.


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).

PlantUML diagram

#PhaseRepositoryEntry CriteriaExit CriteriaTrack
0DNS FoundationinfrastructureNoneassets.arda.cards zone deployed to root; <infra>.assets.arda.cards subdomain zone + ACM cert deployed; DNS resolves; cert status ISSUEDCritical path
1AWS InfrastructureinfrastructurePhase 0 deployedCDK deployed; verify-image-cdn.ts passes (presigning role, S3 upload, CDN 403 without cookies, CDN 200 with cookies, tenant isolation)Critical path
2acommon-module Librarycommon-modulePhase 1 key format and CDN domain defined (CDK synth)S3AssetServiceTest, AssetKeyGeneratorTest, CdnUrlResolverTest pass; artifact published to local MavenCritical path
2boperations EndpointsoperationsPhase 2a artifact publishedImageUploadEndpointTest passes; integration tests pass (presigned POST, CDN URL accepted, non-CDN rejected, null clears); operations buildsCritical path
2cLive Verificationoperations + infrastructurePhase 1 deployed + Phase 2b buildsOperations pod assumes presigning role; presigned POST uploads real image to S3; entity update with CDN URL succeeds; image served from CloudFrontCritical path
3aBFF Proxy & URL Utilsarda-frontend-appPhase 2b endpoint contract definedProxy passes auth/tenant headers; check-url/fetch-url work; SSRF rejects private IPs; rate limiting returns 429; 401 without sessionCritical path
3bBFF Cookie Signingarda-frontend-appPhase 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
4aSPA Componentsarda-frontend-appNone19 components built; Storybook stories render; unit tests pass; mock handlers workComponents
4bGrid + Cookiesarda-frontend-appPhase 3b + Phase 4aThumbnails load from CDN; proactive refresh works; tenant switch re-scopes; 403 recovery works; hover preview on ~500ms; double-click opens editorRead-path
4cUpload + Formsarda-frontend-appPhase 3a + Phase 3b + Phase 4aAll 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 gateCritical path
4dE2E Integrationarda-frontend-appAll prior phasesScenarios S1-S7 verified end-to-end against deployed environmentConvergence
5API Testsapi-testPhases 1-3 deployedBruno test collections pass against live dev environmentIndependent

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