Skip to content

Decision Log

Decisions made during the Item Image Upload project. Each section corresponds to a project phase.

Scoping decisions are recorded in Scoping. Key decisions that shaped the project scope:

IdDecisionOutcomeReference
SD-01Fetch-and-store external URLsResolved — SPA-side fetch-and-store. External URLs are fetched by the SPA, rendered in the editor for crop/zoom/rotate, then uploaded via the managed presigned-upload path. All persisted image URLs originate from managed storage. Resolved by TD-01.T-03, Copyright Consultation, TD-01
SD-02Remove background edit operationOut of scope — documented as future enhancement. Not in V1.T-08
SD-03Bulk CSV image column UXDeferred — separate session.REF::ITM::0006::0005.FS
SD-04Retention policy for superseded imagesDeferred — GitHub ticket to be created during project implementation.T-09
SD-05Cross-tenant image sharing constraintsOut of scope — not in this version regardless of legal resolution. Legal review required before any future implementation.T-12
SD-06Aspect ratio: design-time parameterDecided — locked per entity type; Item = 1:1 square, not user-modifiable.T-05
SD-07Copyright acknowledgment on uploadIn scope — copyright acknowledgment is displayed as inline text in the confirm dialog footer for all input methods. No checkbox or blocking gate. The user is informed that by providing images they accept they have the right to use them. Server-side logging of the acknowledgment is out of scope for this release (TD-04). Superseded by FD-18 which settled the exact UX form.T-11, FD-18
SD-08Camera capture as input methodIn scope — named input method; detailed mobile UX deferred.T-10
SD-09HEIC/HEIF format supportIn scope — pending browser support confirmation before implementation.T-02
SD-10Auto-compression before rejectionIn scope — 85% JPEG quality, max 2048px longest edge.T-07
SD-11Comparison layout and interaction statesIn scope — desktop: small reference + large preview; mobile: two tabs; drop zone affordances; five interaction states.T-04
SD-12Basic edit operationsIn scope — Crop, Zoom, Rotate, Pan, Reset included in V1.T-08
SD-13Cross-cutting concerns documentationIn scope — document existing system behavior: all-user permissions, last-user-wins concurrency, PUT-based bulk CSV semantics.T-06
SD-14Clipboard paste edge casesIn scope — image blob, unrecognized format, and HTML-with-embedded-URL handling are all in scope. With SD-01 resolved (fetch-and-store), embedded URLs are extracted, fetched SPA-side, and uploaded via managed path.T-03, TD-01
SD-15Grid image display and interactionIn scope — thumbnail display, inspector overlay, inline edit entry point, and error-state fallback are in scope as GEN::MEDIA::0003 and GEN::MEDIA::0001::0007.FS. Hover-to-enlarge and lazy loading remain deferred.GEN::MEDIA::0003, 0001::0007.FS
SD-16Mobile camera capture UXOut of scope — deferred to future mobile UX session. Camera capture as input method is in scope (SD-08).T-10

Decisions made during the first review of the UI Components specification.

IdDecisionOutcomeReference
CD-01Rename EntityTypeConfig to ImageFieldConfigDecided — the interface configures an image field, not an entity type. File: image-field-config.ts.components.md — Shared Types
CD-02Rename entityTypeName to entityTypeDisplayNameDecided — avoids confusion with ADN entity type identifiers.components.md — ImageFieldConfig
CD-03Add propertyDisplayName to ImageFieldConfigDecided — entities may have multiple image fields (e.g., product image, logo). The property display name provides context in user-facing messages.components.md — ImageFieldConfig
CD-04Strongly type acceptedFormats with ImageMimeType unionDecidedImageMimeType union of known MIME types replaces string[]. Compile-time validation, autocomplete, prevents typos. Extending for new formats requires a conscious type update.components.md — ImageFieldConfig
CD-05Defer Avatar rewrite (Option B) to follow-upDeferred — Avatar stays as-is (only getInitials extraction). Full rewrite to compose ImageDisplay deferred until ImageDisplay is stable in production. Documented in Follow-Up Work.follow-up-work.md
CD-06Drop ItemImageField / EntityImageField organismDecided — no wrapper organism needed. Configuration specialization is a constant (ITEM_IMAGE_CONFIG) passed to generic components. ImageFormField (molecule) is used directly. The Organism-Item layer is eliminated.components.md — Overview
CD-07ImageInspectorOverlay supports edit transitionDecided — optional onEdit callback. When present, shows “Edit” button. In grid context, caller wires to api.startEditingCell(). Inspector closes itself and AG Grid’s cell editor lifecycle takes over.components.md — ImageInspectorOverlay
CD-08Unified hover-based interaction modelDecided, then revised. Original: hover reveals action icons (eye, pencil, trash). Post-project revision (usability testing): Action icons removed — too intrusive for the small grid cell space. Final model: single click = row selection (AG Grid default). Hover (~500ms) = ImageHoverPreview popover (larger preview only, no action icons). Double-click / Enter = edit flow (ImageUploadDialog). In form contexts, edit is triggered via double-click on the image area; remove via a control outside the image cell. ImageInspectorOverlay (full-size modal) is available in non-grid contexts (detail panel, form) but not triggered from grid hover icons.components.md — Component Composition
CD-09Add ImageHoverPreview componentDecided — new molecule. Lightweight popover for quick image inspection on hover. Not a modal — no focus trap. Dismisses on mouse-out. Addresses the need for casual image inspection without consuming the single-click gesture.components.md — ImageHoverPreview

Decisions made during specification of the Use Case Stories.

IdDecisionOutcomeReference
UD-01REF::ITM::0004::0006.UC component sourcingDecided — compose from canary components only (ItemGrid, ItemDetails, ImageFormField, ImageUploadDialog, canary app shell). No vendored components. The canary grid and detail panel provide sufficient context to demonstrate image change and removal without pulling in the 2,483-line vendored ItemFormPanel.specification.md — REF::ITM::0004::0006.UC
UD-02Vendored globals.css scoped to reference story onlyDecided — the vendored CSS (@/styles/vendored/globals.css) is required only by the reference story in REF::ITM::0003::0010.UC because ItemFormPanel uses Tailwind classes that need the vendored @source scan directive. All other stories (including all GEN::MEDIA, REF::ITM::0004::0006.UC, and the canary variant of 0003::0010.UC) run on the canary globals.css already loaded globally by preview.ts. The canary CSS provides all design tokens (oklch-based) and Tailwind configuration. The vendored CSS must not be imported in any story that does not render vendored components.specification.md — Vendored Fallback
UD-03REF::ITM::0003::0010.UC dual-story approachDecided — two stories for this scenario. (1) A reference story preserving the vendored ItemFormPanel unmodified, with its own embedded image handling (URL text field, not canary components). Purpose: baseline comparison of current production form. (2) A canary story (0010 Set Image During Creation – Canary) with a story-local simplified Item form composed entirely from canary components (Input, Label, Button, ImageFormField, ImageUploadDialog). The canary story demonstrates the project’s image upload components as first-class form participants. The vendored form’s monolithic architecture (2,483 lines, 8 domain typeaheads, embedded image state machine) makes adapter-based integration non-trivial and fragile; a clean canary composition provides a better demonstration of the target UX. Consequence: The vendored reference story (during-creation.stories.tsx) imports from canary-refactor/ which uses @frontend/ aliases that only resolve in Storybook’s Vite config. This file must be excluded from tsconfig.json to avoid tsc failures in CI. Stories are not production code, so reduced type coverage is acceptable.specification.md — REF::ITM::0003::0010.UC

Decisions made during the System Design session. Design decisions TD-01 through TD-07 are recorded in the system design index. The entries below record the resolution of previously deferred scoping decisions and new decisions that arose during system design authoring.

IdDecisionOutcomeReference
TD-08Presigned POST (not PUT) for uploadsDecided — the system uses presigned POST (form-based upload) rather than presigned PUT. Presigned POST enforces Content-Type and Content-Length server-side via S3 policy conditions, providing defense-in-depth that presigned PUT cannot. This is a system-level decision affecting the contract between SPA, Backend, and AWS.system-design/index.md, prior research
TD-09SD-01 resolved: fetch-and-store is SPA-sideResolved — TD-01 resolves the previously deferred SD-01 decision. External HTTPS URLs are fetched by the SPA, rendered in the image editor, then uploaded via the managed presigned-upload path. All persisted image URLs originate from managed storage. External URLs are never persisted directly. Feature requirements (GEN::MEDIA::FR-0020) and use case specifications updated accordingly.TD-01, SD-01
TD-10SD-14 resolved: clipboard HTML-with-embedded-URL fully in scopeResolved — with fetch-and-store decided (TD-01/TD-09), the clipboard paste edge case for HTML-with-embedded-URL is fully resolvable. The SPA extracts the image URL from clipboard HTML markup, fetches it, and routes through the managed upload path.TD-01, SD-14
TD-11CDN access control via CloudFront signed cookiesDecided — product images are sensitive (customers may upload R&D prototype photos). CDN access requires authentication via CloudFront signed cookies scoped to the active tenant’s key prefix (/<tenantId>/*). Cookies are issued by the BFF, validated at the CloudFront edge (sub-ms, no Lambda@Edge). Cookies are not part of the cache key, so CDN caching is fully preserved. This replaces the earlier assumption (A-001) that product images are not sensitive.system-design results, prior research
TD-12Signed cookie lifecycle: short-lived with proactive renewalDecided — signed cookies have a short TTL (default 30 minutes, configurable) to minimize breach window. The SPA proactively refreshes cookies before expiry (~50% of TTL). On tenant switch (which can occur without re-authentication), the SPA immediately requests new cookies scoped to the new tenant. Until new cookies arrive, image requests to the new tenant return 403; the SPA shows loading state during the brief refresh window.TD-11
TD-13Module-scoped upload endpoint pathDecided — the presigned upload credential endpoint is module-scoped under the item module path: POST /v1/item/image-upload (Backend) / POST /api/item/image-upload (BFF). The generic POST /v1/storage/upload-url with module and entityType body fields was rejected — the entity type is conveyed by the path (under /v1/item/), which is sufficient since presigned credentials are module-level operations that do not require per-entity identity in the path. This aligns with the existing Arda REST API pattern where module-scoped operations are grouped under the module resource path.system-design results
TD-14No separate ImageUrlValidator in operations — use CdnUrlResolver directlyDecided — the original design included an ImageUrlValidator class in operations that wrapped CdnUrlResolver.validate() from common-module. Since CdnUrlResolver already validates the CDN host pattern, key format, and tenant prefix, the wrapper added no logic — only indirection. ItemService calls CdnUrlResolver.validate() directly. Rationale: DRY — when a common-module class already provides the exact validation needed, adding a per-consumer wrapper in each component creates maintenance burden without value. Future entity types adopting image fields should also call CdnUrlResolver directly rather than creating their own validator wrappers.backend-specification.md

Project Design — Backend Services (2026-03-31)

Section titled “Project Design — Backend Services (2026-03-31)”

Decisions made during the Backend Services goal definition.

IdDecisionOutcomeReference
TD-15Grandfather existing imageUrl values on entity updateDecided — the imageUrl CDN pattern validation (BE-FR-003) applies only when the imageUrl value is modified relative to the currently persisted value. If the imageUrl field is unchanged in the update payload, validation is skipped. This ensures that items with pre-existing non-CDN image URLs (e.g., manually entered URLs from before this feature) are not broken by the new validation logic. The comparison is value-based: if payload.imageUrl == persisted.imageUrl, skip CDN validation and HEAD verification.goal.md, BE-FR-003
TD-16URL format reference for backend implementationDecided — the following URL and key formats are the canonical contracts for the backend services. All are defined in the AWS Specification and Backend Specification; consolidated here for implementer reference. S3 Object Key: <tenantId>/images/<uuid>.<ext> — tenant-first, feature-namespaced, UUID per upload. CDN URL: https://<partition>.<infra>.assets.arda.cards/<tenantId>/images/<uuid>.<ext> — constructed by CdnUrlResolver from object key + CDN host. S3 Upload URL: https://<bucket>.s3.<region>.amazonaws.com — target for presigned POST form submission. Arda-Key metadata: operations/item/imageUrl/<uuid>.<ext> — follows the Static Asset Repository Arda-Key convention. Content-Type → extension mapping: image/jpegjpg, image/pngpng, image/webpwebp, image/heicheic, image/heifheif. Presigned POST policy conditions: key (eq), Content-Type (starts-with image/), content-length-range (1 to max), x-amz-meta-tenant-id (eq), x-amz-meta-author (eq), x-amz-meta-arda-key (eq), x-amz-server-side-encryption (eq AES256). Signature duration: 15 minutes (configurable, aligned with uploadSignatureDuration).aws-specification.md, backend-specification.md, TD-08
TD-17S3AssetService supports both presigned PUT and POSTDecidedS3AssetService is the core S3 management class offering both presignedPutUrl() (for CSV and future consumers) and createPresignedPost() (for image upload). Role assumption is managed internally: default credentials for PUT operations, assumed presigning role for POST operations. This isolates future IAM role consolidation to a single class. CsvS3BucketDirectAccess delegates its presigned PUT generation to S3AssetService.analysis.md section 2.1, BE-FR-001, TD-08
TD-18CsvS3BucketDirectAccess delegates all S3 primitives to S3AssetServiceDecided — aggressive delegation. CsvS3BucketDirectAccess receives S3AssetService as a constructor dependency and delegates presigned PUT, HEAD, and metadata validation. Only CSV-specific code remains: GET with decompression, row/batch flow parsing, compression handling, and CSV key construction. This promotes cohesive abstractions — S3 interaction primitives belong in one class. Existing CsvS3DirectAccessTest and S3BucketAccessTest must pass without modification.analysis.md Category 3, REQ-BE-020
TD-19AssetKeyGenerator is a class with configurable, validated feature namespaceDecidedAssetKeyGenerator is instantiated with a featureNamespace parameter (e.g., "images") that is validated at construction time as a URL path segment compatible format. Construction fails fast if the namespace is invalid. This supports future asset types (e.g., "documents") without code changes. The configurable namespace pushes the design toward a class rather than a bare function.analysis.md section 1.3, BE-FR-002
TD-20TenantId retrieved from ApplicationContext (Kotlin coroutine context)Decided — request-scoped tenant information is retrieved from ApplicationContext, a coroutine context element populated by a Ktor plugin on every request. This follows the established ScopedUniverse pattern in common-module. All relevant code is in common-module. This is the mechanism of choice for accessing request contextual information, which will expand in the future to include Access Decision assertions.analysis.md sections 1.2, 1.3, 1.5
TD-21BE-FR-003 backward-compatibility note for TD-15 grandfatheringDecided — the backend specification’s BE-FR-003 (CDN URL validation) is updated with a backward-compatibility note referencing TD-15. Use case documents used for User Acceptance Testing may not have access to project design decision details, so the exception must be visible in the specification itself.backend-specification.md, TD-15
TD-22Image upload routes integrated into ItemEndpoint, not standaloneDecided — option (b). Image upload presigned credential routes are registered within ItemEndpoint.buildRoot() as imageUploadRoutes(), following the existing printingRoutes() / supplyRoutes() pattern. This keeps all item-scoped routes in one place, ensures they appear in the OpenAPI specification under the Item resource, and benefits from ItemEndpoint’s existing ItemService injection. A standalone endpoint class would add file bloat without value since the endpoint is thin (delegates to ItemService).specification.md OQ-2, TD-13
TD-23Image URL validation in ItemValidator.validateForUpdate, not ItemServiceDecided — the imageUrl CDN validation and HEAD verification are implemented in ItemValidator.validateForUpdate() (persistence/ItemValidator.kt), not as a separate step in ItemService.update(). The validator already receives ctx: ApplicationContext (provides tenant via TD-20), payload: Item (new value), and previous: BitemporalEntity<Item, ItemMetadata>? (persisted value for TD-15 grandfathering comparison). This avoids additional DB trips, ensures the values evaluated are correct (the universe provides the previous entity), and follows the established validation pattern where ScopingValidator subclasses enforce domain invariants. validateForCreate() is also extended for the create path (where previous is null, so grandfathering does not apply).specification.md OQ-3, ItemValidator.kt
TD-24CsvS3BucketDirectAccess receives S3AssetService via injection, not internal creationDecidedCsvS3BucketDirectAccess factory methods and constructors receive S3AssetService as a parameter rather than creating it internally. This avoids initialization issues (e.g., S3AssetService requires presignRoleArn which the CSV flow does not have) and ensures a single S3AssetService instance is shared across the module. Existing tests must supply an S3AssetService instance — the factory signature changes.specification.md T-5, TD-18
TD-25Verify ItemValidator DI precedent before converting from object to classDecidedItemValidator is currently a stateless object singleton. Adding CdnUrlResolver and S3AssetService dependencies (TD-23) requires converting it to a class instantiated in Module.kt. Before implementing, verify whether any other validator in the codebase takes dependencies (precedent check). If this is the first, document the pattern as a reference for future validators. Resolved: BusinessAffiliateValidator is a class (not object) that takes a universe constructor parameter — confirmed precedent for stateful validators. Proceed with objectclass conversion as spec’d.specification.md T-8, TD-23

Pre-Implementation Decisions — Backend Services (2026-03-31)

Section titled “Pre-Implementation Decisions — Backend Services (2026-03-31)”

Decisions made during pre-implementation review of the Backend Services specification, before launching Run 1.

IdDecisionOutcomeReference
TD-26AWS SDK upgrade + manual presigned POST implementationDecided — upgrade AWS SDK from 2.34.3 to 2.42.24 and implement presigned POST policy construction as an internal utility within S3AssetService. AWS SDK for Java v2 has no native presigned POST support (#1493, open since 2019; #6577, Nov 2025). No v3 SDK is announced. Three options evaluated: (a) upgrade + manual signing, (b) third-party library aws-s3-presigned-post v1.0.1, (c) upgrade + manual signing encapsulated as internal utility. Option (c) chosen: keeps dependency footprint minimal in common-module, avoids reliance on low-maturity third-party library (single contributor, v1.0.1), and the signing logic is well-documented by AWS. Implementation must include extensive comments and references to authoritative AWS documentation (SigV4 POST policy, POST form fields). Also requires adding aws-sdk2-sts to common-module’s libs.versions.toml and build.gradle.kts for STS AssumeRole.specification.md T-4, TD-08, TD-17
TD-27Gradle includeBuild uses relative pathDecided — the commented includeBuild("../common-module") line in operations/settings.gradle.kts is uncommented for development. Uses relative path (../common-module) for portability across directory layouts. Must not be committed — CI resolves common-module from GitHub Packages.specification.md T-6, Goal constraint #2
TD-28MockAWS STS: LocalStack first, MockK fallbackDecidedS3AssetServiceTest should add STS to the MockAWS service list if supported by the LocalStack image (option a). If LocalStack does not support STS or role assumption, fall back to mocking the STS call with MockK (option b). Do not test with default credentials as they may not be available in CI. Escalate to user if neither approach works.specification.md T-4, TD-17
TD-29Commit strategy: one commit per verified taskDecided — each task in Run 1 (T-2, T-3, T-4, T-5) produces an independent commit after its tests pass. This provides clear rollback points and aligns with the test-first ordering constraint.specification.md Goal constraint #8
TD-30PRs target main, not jmpicnic/claude-prepDecided — both common-module and operations PRs target main. The jmpicnic/claude-prep branch is expected to merge into main before the backend services PRs are ready. Run 2 must start with the working branch up-to-date with (or ahead of) main.choreography.md

Project Design — AWS Infrastructure (2026-03-30)

Section titled “Project Design — AWS Infrastructure (2026-03-30)”

Decisions made during the AWS Infrastructure detailed design phase.

IdDecisionOutcomeReference
PD-01Image CDN: new stack vs. extend PurposeIngressDecided — new ImageStorageStack rather than extending the existing PurposeIngress stack. The image CDN has a different lifecycle (RETAIN bucket, long-lived caches, signed cookie key group) from the API Gateway ingress. Separating them follows the existing pattern where BulkStoresStack is independent from PurposeIngress even though both serve the same partition. This also enables independent deployment and avoids growing the already complex ingress stack.specification.md, design.md
PD-02CDN domain: assets.arda.cards (not io.arda.cards)Decided — the image CDN uses a dedicated assets.arda.cards domain family with the pattern <partition>.<infrastructure>.assets.arda.cards (e.g., demo.alpha001.assets.arda.cards). API traffic (io.arda.cards) and static asset delivery target fundamentally different capabilities: different security models (JWT vs. signed cookies), different caching strategies (disabled vs. aggressive), and different content types (JSON vs. binary images). Sharing the io domain would conflate these concerns. This requires new Route53 hosted zones at root and infrastructure levels, plus a new ACM wildcard certificate per infrastructure — added to Phase 1 scope.specification.md, design.md, aws-specification.md
PD-03Root account deployment: deploy-root.sh scriptDecided — a minimal deploy-root.sh script covers root account CDK deployment (bootstrap + deploy r53-zones.ts). The root account is not covered by amm.sh and has no automation today (manual npm run deploy:plroot). The script follows the same patterns as amm.sh but scoped to the single root CDK app. CI automation (GitHub Actions workflow for root) is out of scope for this project — it requires OIDC role setup for the root account.specification.md
PD-04Image bucket co-located with CDN in ImageStorageStackDecidedImageAssetBucket is co-located with the CDN in ImageStorageStack (not in BulkStoresStack). CDK’s S3BucketOrigin.withOriginAccessControl() auto-adds a bucket policy referencing the distribution — when bucket and distribution are in different stacks, this creates a cyclic CloudFormation reference. Co-locating them in the same stack eliminates the cycle. This is architecturally better: bucket, CDN, and signing keys form a cohesive unit with shared lifecycle (RETAIN). BulkStoresStack is unchanged from its original state.specification.md, design.md
IdDecisionOutcomeReference
FD-01TanStack Query component binding: typed providers vs. TanStack-aware componentsDecided — design system components (ux-prototype / @arda-cards/design-system) use Option C: mandatory typed data provider strategy. Components define typed interfaces for their data needs (hooks returning { data, isLoading, error }); the consuming app (arda-frontend-app) provides TanStack Query–backed implementations. Components never import @tanstack/react-query directly. This ensures full tsc-time type safety at assembly, keeps the design system backend-agnostic and portable, centralizes cache strategy in the app, and enables Storybook/test usage without QueryClientProvider. For app-specific “local” components in arda-frontend-app that are not part of the design system, inline useQuery/useMutation (Option A) is acceptable as a pragmatic choice since they are not reused outside the app.tanstack-component-binding-analysis.md
FD-02Code organization for arda-frontend-app: layers vs. features vs. hybridDecidedOption C: Hybrid (layers with feature grouping). Top-level directories map to architectural layers (server/, api/, hooks/, components/, store/, shared/, providers/), each serving as an eslint-plugin-boundaries zone with enforced import rules. Feature grouping happens within each layer (hooks/items/, hooks/image-upload/, components/items/). BFF code isolated in src/server/ with import 'server-only'; SPA API functions in src/api/; shared code in src/shared/. Dependency direction: pages → ui/providers → hooks → api/store → shared; bff → shared (isolated). Migration is incremental: Phase 1 creates server/, api/, shared/ (#734); Phase 2 adds ESLint boundary rules; Phase 3 groups by feature within layers. The src/server/ directory includes a lib/ subdirectory for server-only utilities (e.g., cloudfront-signer.ts, ssrf-validator.ts, rate-limiter.ts). A future src/client/lib/ directory may be introduced when the existing src/lib/ contents are reorganized (#734), but is not required for this project.code-organization-options.md
FD-03Lifecycle framework scope in image upload projectDecided — minimal: only what image upload needs. Introduce ValidationResult, FieldError, EditLifecycleCallbacks<T>, EditableComponentProps<T>, and useDraft<T> types/hook. Update only the 6 image-related moderate-change components (not the 13 minor components). useComposedDraft<T>, createCellEditorFactory<T>, and the full validation composition framework are deferred to a follow-up project.ux-prototype-architectural-review.md
FD-04Upload progress indicator: simulated vs. real vs. indeterminateDecidedOption C: indeterminate indicator now, optional progress later. Replace the simulated 50ms timer with an indeterminate progress indicator (spinner or pulsing bar). The onUpload callback keeps its current signature (file: Blob) => Promise<string>. Real progress can be added later as a non-breaking optional parameter: onUpload: (file: Blob, options?: { onProgress?: (pct: number) => void }) => Promise<string>.upload-component-backend-analysis.md
FD-05Entity persist after image upload: Redux vs. TanStackDecided — stay in Redux. The entity update after upload uses the existing ardaClient.updateItem() → Redux thunk → itemsSlice pattern. TanStack Query is introduced for image-specific operations (upload credentials, reachability, CDN cookies) but does not replace the entity CRUD flow in this project.
FD-06ItemCardEditor production wiringDecided — out of scope. The ItemCardEditor organism is prototype-only for now. Production wiring (typed provider hooks, FD-01 compliance) deferred to a future project.
FD-07Legacy cleanup sequencing (@tanstack/react-table removal)Decided — first commit in the arda-frontend-app branch. Remove ItemTable.tsx, its test, and the @tanstack/react-table dependency before adding new code. All checks and tests must pass after this commit.
FD-08api-proxy: Node version for crypto.randomUUID()Decided — use crypto.randomUUID() and update api-proxy engines.node to >=20.0.0 (from >=18.18.0). arda-frontend-app runs Node 20.19.0, AWS Amplify supports Node 20 and 22 (Node 18 deprecated September 2025). No manual UUID v4 implementation needed.3.1-api-proxy-publish/specification.md
FD-09api-proxy: RequestOptions export locationDecided — export RequestContext and RequestOptions from @arda-cards/api-proxy/shared only. Not re-exported from domain barrels. Callers import directly from the shared entry point.3.1-api-proxy-publish/specification.md
FD-10CloudFront signing key: env var vs. Secrets Manager at runtimeDecided — use AWS SDK SecretsManagerClient.GetSecretValue() at runtime, not environment variables. AWS explicitly recommends against storing private keys in env vars. The Amplify SSR Compute Role (already deployed for Alpha001 demo; other envs via infrastructure#438) grants secretsmanager:GetSecretValue. The secret name is derived from the naming convention utility (${Infrastructure}-${Partition}-ImageCdnSigningKey) — no secret ARN env var is needed. Only CLOUDFRONT_KEY_PAIR_ID, NEXT_PUBLIC_INFRASTRUCTURE, and NEXT_PUBLIC_PARTITION are required as env vars (the NEXT_PUBLIC_ prefix makes infrastructure names available to both server and client code; non-prefixed fallback supported for backward compatibility). The PEM key is fetched from Secrets Manager using the derived name and cached in memory (8-hour TTL).3.5-bff-routes/specification.md
FD-11REQ-FE-020 (no file byte buffering) enforcementDecided — REQ-FE-020 (“BFF shall not buffer or store image file bytes”) is verified by code review on every BFF PR. A future structural lint rule may automate this check, but for now it is a documented review obligation.audit-1.md finding #19
FD-12BFF error response format: user-facing text vs. machine-readable codesDecided — BFF routes return machine-readable error codes (e.g., { "code": "RATE_LIMITED", "retryAfterMs": <value> }) instead of user-facing text. The SPA API layer (src/api/) is responsible for translating codes into localized, user-facing messages. This decouples UI copy from the BFF, enabling localization and wording changes without BFF redeployment.audit-1.md finding #16
FD-13SPA types: independent definition vs. re-export from api-proxyDecided — SPA request/response types (ImageUploadRequest, ImageUploadResponse) are re-exported from @arda-cards/api-proxy/reference/item through a shared types barrel, not defined independently. Since api-proxy is already a dependency of arda-frontend-app (used by BFF routes), re-exporting types does not introduce a new dependency. This ensures compile-time contract alignment between the SPA API layer and the Backend.audit-1.md finding #20
FD-14useDraft<T> initialValue reset: reference equality with opt-in isEqualDecideduseDraft<T> resets the draft when initialValue changes, using reference equality (Object.is) by default. An optional isEqual: (a: T, b: T) => boolean parameter allows callers to use structural comparison when needed. When using reference equality, the parent must memoize initialValue to prevent unwanted resets on re-render. This follows React conventions (similar to useEffect dependency arrays).audit-1.md finding #14
FD-15Design system factory hooks: required in types, stubs in StorybookDecidedImageCellEditorConfig makes useImageUpload and useCheckReachability required fields (not optional). Storybook stories provide stub implementations explicitly. This shifts missing-hook errors from runtime console.warn to compile-time tsc failure — production column definitions that forget to wire hooks fail the build, not silently at runtime.audit-1.md finding #26
FD-16CDN image 403 recovery: shared useImageWithCdnRecovery hookDecided — a shared useImageWithCdnRecovery(cdnUrl, options?) hook in src/hooks/cdn/ provides 403 recovery for all image display contexts (grid, form, inspector). The hook detects CDN URLs, calls refreshNow() from CdnCookieContext on error, and cache-busts the src for retry. ImageDisplay stays pure (FD-01 compliant) — the hook is wired at the app level, not in the design system. Maximum one retry per URL per mount to prevent refresh storms.audit-1.md finding #11, 3.6-spa-integration T-7b
FD-17CloudFront CORS for canvas-based image editingDecided — the ImageStorageStack CloudFront distribution requires a Response Headers Policy with CORS configuration: Access-Control-Allow-Origins: https://*.arda.cards, Access-Control-Allow-Credentials: true, Access-Control-Allow-Methods: GET, HEAD. This is required because the edit-existing-image flow loads CDN images onto an HTML canvas (react-easy-crop), which triggers cross-origin restrictions. The SPA sets crossOrigin="use-credentials" on the crop <img> element only for CDN URLs (not for local blobs). Display-only <img> tags (thumbnails, previews) do not set crossOrigin to avoid unnecessary CORS preflights.infrastructure#439, audit-1.md finding #28
FD-18Copyright acknowledgment: inline text, no checkboxDecided — the copyright acknowledgment is displayed as inline text in the ImageUploadDialog confirm dialog footer, stating that by providing images the user accepts they have the right to use them. The confirm button is always enabled — there is no checkbox gate or blocking acknowledgment step. This simplifies the UX while still providing the legal notice. Server-side logging of the acknowledgment is out of scope (TD-04). Supersedes SD-07’s original “mandatory checkbox/click” wording.audit-2.md finding #11, TD-04, SD-07
FD-19Image grid edit-trigger: workaround for AG Grid 34.3.1 cell-editor mount crashSuperseded by FD-20. Originally: flatten createImageCellEditor from a double-forwardRef (factory wrapper around a forwardRef editor) to a single forwardRef so AG Grid’s React adapter sees isPopup() => true synchronously. AG Grid 34.3.1’s TooltipService.setupCellEditorTooltip early-returns when editor.isPopup?.() returns truthy; otherwise it constructs an AgTooltipFeature whose postConstruct → refreshTooltip calls this.beans.gos.get(...) on an unwired bean and crashes with Cannot read properties of undefined (reading 'get') at ag-grid-community/dist/package/main.esm.mjs:8752. The single-forwardRef flattening eliminated the tooltip crash, and the defensive URL.createObjectURL guard in ImagePreviewEditor eliminated a secondary “Overload resolution failed” error, but the dialog still failed to become visible in the user’s real browser: the cellEditorPopup:true column-def wrapped the editor in an AG-Grid-owned 0×0 popup container, and removing cellEditorPopup made the editor inline — either way, when the Radix Dialog portals out of the cell’s focus scope, stopEditingWhenCellsLoseFocus:true unmounts the editor before the user can interact. The isPopup() runtime handle suppresses AG Grid’s tooltip feature but does not exempt the editor from focus-loss teardown when the column def does not also declare cellEditorPopup:true. The single-forwardRef flattening and Blob guard are retained as useful hardening for library consumers, but the app-level wiring moves to FD-20.analysis.md Issue 2; ag-grid#10335; ag-grid#10372
FD-20Image grid edit-trigger: bypass AG Grid cell-editor lifecycleDecided — the image column in columnPresets.tsx is no longer editable. Double-click, Enter, and F2 on an image cell are captured at the grid level (onCellDoubleClicked, onCellKeyDown) and trigger a wrapper-scoped ImageUploadDialog controlled by state in ItemTableAGGrid. On confirm, the wrapper writes the new URL via api.applyTransaction({ update: [...] }) and marks the row dirty through the existing dirtyRowIdsRef / publishRow pipeline used by onNotesSave and onCardNotesSave. This means image edits integrate with other cell edits in the same row through a single debounced publish (one save per row, not per field), matching the model other rich-interaction columns already use. Reason: AG Grid’s edit lifecycle assumes the editor renders within the cell’s focus scope. A Radix-portalled modal editor violates that assumption in both cellEditorPopup configurations: with cellEditorPopup:true the editor is wrapped in a 0×0 AG-Grid-owned container whose hidden subtree defeats Radix focus-trap; without it, stopEditingWhenCellsLoseFocus:true unmounts the editor as soon as the Radix portal steals focus. FD-19’s flattened single-forwardRef eliminated one crash class (tooltip bean) but could not resolve the focus-scope impedance mismatch — which is structural, not a bug. Shape of the change: (1) columnPresets.tsx image column: remove editable:true and cellEditor; keep cellRenderer:ImageCellDisplay. (2) ItemTableAGGrid.tsx: add `const [imageDialog, setImageDialog] = useState<{row, rowIndex}null>(null), add onImageSave(item, newUrl)following the exact pattern ofonNotesSave/onCardNotesSaveat:1134/:1150, add onCellDoubleClicked/onCellKeyDownhandlers that open the dialog when the column isimageUrl, render controlled byimageDialog. (3) gridContextexposesopenImageEditor(row, rowIndex)so the cell renderer can also trigger it (consistent with existingonNotesSavewiring). **Alternatives considered and rejected:** (a)stopEditingWhenCellsLoseFocus:false— too broad; breaks inline editors in other columns. (b) Keep FD-19 and wait for AG Grid 34.x patch — no public bug tracked; indefinite timeline. (c) Downgrade AG Grid — high blast radius. (d) Reparent Radix Dialog into the grid's focus scope — fights Radix's portal model; fragile. **Tradeoffs:** image column is no longer "editable" via AG Grid APIs (e.g., programmaticapi.startEditingCell(‘imageUrl’)no longer opens it), but this API is not used anywhere in the repo. Enter/F2 bindings move from AG Grid defaults to explicit handlers for this column.createImageCellEditorandImageCellEditorremain exported from the ux-prototype library (kept for Storybook and any future consumer that can tolerate the AG Grid lifecycle — e.g., a grid configured withoutstopEditingWhenCellsLoseFocus). The single-forwardRefandBlob` guard from FD-19 remain as library-level hardening.
FD-22Dual-mode CDN auth: signed URLs for non-*.arda.cards origins (Approach D)Decided — the backend stores plain CDN URLs (https://<partition>.<infra>.assets.arda.cards/<tenant>/images/<uuid>.jpg). The SPA resolves them at display time via CdnAuthProvider: on *.arda.cards origins (production), cookie-based auth (identity pass-through, zero cost); on all other origins (Amplify preview, localhost), per-image signed URL via BFF GET /api/storage/sign-url. CloudFront natively accepts both signed cookies and signed URLs — no distribution config change needed for auth. Two levels of CDN image access: (1) Display (<img src>) works everywhere with signed URLs. (2) Canvas-based editing (react-easy-crop in ImagePreviewEditor) requires CORS headers from CloudFront because the <img> element has crossOrigin="use-credentials" (FD-17). Signed URLs authenticate the request but do not satisfy the CORS check — the CloudFront Response Headers Policy must include the page’s origin. On production (*.arda.cards) CORS is permanent. On preview branches (*.amplifyapp.com) CORS is permanent per infrastructure#442. On localhost CORS requires a temporary CLI change (knowledge-base documented). Shape: BFF route GET /api/storage/sign-url (JWT-authenticated, tenant-scoped, SSRF-guarded, same signing key as cdn-cookies); getSignedImageUrl() SPA API function; CdnCookieProvider upgraded with resolveImageUrl() + isCookieMode; useResolvedImageUrl(cdnUrl) hook (zero-cost in cookie mode); ResolvedImage/ResolvedImg utility components; ResolvedImageCellDisplay wrapper for AG Grid; all image render sites in the app use the hook. No ux-prototype changes. No infrastructure changes for auth (CloudFront accepts signed URLs natively).analysis.md; FD-17; infrastructure#442
FD-21Industry-pattern validation for FD-20 (portal-based rich cell editors)Decided — FD-20’s approach (grid becomes trigger, host owns lifecycle, commit flows back through the grid’s public transaction API) is the mainstream recommendation for portal-based rich cell editors, not an Arda-specific workaround. This entry records the external sources that validate the choice so future readers can evaluate whether the AG-Grid-specific reasoning in FD-20 is still the right one if the grid library is swapped or upgraded. Core principle (restated across sources): if the editor owns focus in a way the grid cannot observe, take it out of the grid’s edit state machine. The grid becomes the trigger; the host owns the editor lifecycle; the committed value flows back through the grid’s public transaction API. Evidence by library: (1) AG Grid — the React cell-component guide notes that portal-rendered editors pay a focus-loss cost; community issues #4556 (cell editor with Portal/Dialog) and #6238 (stopEditingWhenCellsLoseFocus breaks popup editors) describe the same impedance mismatch FD-20 resolves; the recommended paths are full-row edit mode or event-level interception with applyTransaction commit. (2) MUI X DataGrid — documents renderEditCell + portal combinations but warns explicitly that for modals developers should “consider raising the editor out of the cell edit state machine and committing via updateRows — the same prescription as FD-20. (3) Glide Data Grid — ships an explicit “overlay editor” split: the grid provides trigger events, a separate overlay editor owns focus and lifecycle, and commits bubble back via the grid’s public API. (4) Handsontable — its BaseEditor subclass for modal-style edits requires the editor to manage its own focus trap and call finishEditing(false) explicitly; the documented recipe for image/rich fields is a host-owned popup that signals the grid. (5) Tabulator and TanStack Table (headless) — both trivially support the pattern because edit-state is consumer-owned; TanStack’s own dialog-editor examples hoist the dialog to the table owner and commit via setRowData. Where FD-20 diverges from the sources and why: (a) we trigger from grid events (dblclick/F2/Enter) rather than a per-cell action button — the expectation of “dblclick to edit” is established for other columns in the same grid, so consistency wins; a future action-button trigger is accommodated by the #748 ticket’s ColumnEditSpec.triggers array. (b) we mount one dialog at wrapper level rather than one per cell renderer — matches the MUI X / Glide recommendation and avoids N dialogs in the DOM; the renderer-owned pattern remains available for columns whose trigger is a visible button. Implications: (1) if AG Grid later publishes a patch that makes portal editors focus-safe (no public tracker as of 2026-04-13), FD-20 can be reconsidered but the cost of reverting is moderate — the applyTransaction commit path is still correct, only the trigger-to-editor wiring changes. (2) if we swap grid libraries, the FD-20 pattern ports 1:1 because the only grid API it depends on is “row-update transaction” — every library in the list above exposes an equivalent. (3) the broader ColumnEditSpec work captured in arda-frontend-app#748 follows the same industry pattern (MUI X v8 roadmap calls it “rich edit cells”) — we are not inventing vocabulary, we are adopting it. Follow-ups from this analysis: ColumnEditSpec work is captured in arda-frontend-app#748; no immediate code change required for FD-20 itself.analysis.md Issue 2; FD-20; arda-frontend-app#748; ag-grid#4556; ag-grid#6238; MUI X DataGrid editing; Glide Data Grid
IdDecisionOutcomeReference
FD-23Documint CDN authorization approach: signed URLs vs. proxy vs. IAM principal vs. server-side renderDecided — Option A: CloudFront signed URLs at print time. The operations backend generates a per-image CloudFront signed URL (query-string authentication) with a configurable TTL (default 15 minutes) before sending the template payload to Documint. Documint fetches the image using the signed URL without needing cookies, auth headers, or IP allow-listing. Alternatives rejected: (B) BFF proxy — adds bandwidth through the BFF and requires a Documint-to-BFF auth model; (C) Documint IAM principal — Documint’s egress IPs are not documented as stable; weakens private-by-default posture; (D) Server-side render — large scope; Documint is already integrated for text/label flow; (E) Per-print-job rotating tokens — collapses into Option A in practice because CloudFront custom policies only support DateLessThan, DateGreaterThan, and IpAddress conditions (no job-token condition natively).analysis.md — Issue 4, documint-integration.md
FD-24Signing failure mode: fail fast vs. silent degradationDecided — fail fast at all levels. Missing signing key config or PEM secret fails component startup. Runtime signing errors propagate as Result.failure through the print call chain to the endpoint. No silent degradation to bare URLs — a bare URL will always 403 at Documint, so the print request must fail explicitly rather than produce a broken PDF.documint-integration.md
FD-25Signer placement: common-module vs. operationsDecided — common-module. The CloudFront signing infrastructure is shared across the platform (the BFF already has a TypeScript implementation). Placing the Kotlin CloudFrontUrlSigner in common-module makes it available to any backend component that needs signed URLs in the future without duplicating the crypto logic.documint-integration.md
FD-26Signed URL TTL configurationDecided — configurable via application.conf with Helm override. Default 15 minutes in extras.printing.signedUrlTtlMinutes, overridable via configmap at deploy time. Avoids code changes for operational tuning.documint-integration.md

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