API responses now include file_url and thumbnail_url fields. When S3_PUBLIC_BASE_URL is configured, these point to the CDN domain; when unset, they fall back to the existing API proxy paths so local dev requires no additional setup. UI updated to use response URL fields directly instead of constructing proxy URLs client-side. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.2 KiB
Feature Specification: CDN Image Serving
Feature Branch: 014-r2-cdn-serving
Created: 2026-05-08
Status: Draft
Input: User description: "R2 CDN image serving with local dev fallback to API proxy"
Overview
Images and thumbnails are currently served by proxying bytes through the API. This feature changes image delivery so that clients receive direct URLs pointing to a CDN edge network, eliminating the API as a middleman for image content. In local development, where no CDN is available, the API proxy endpoints remain as a fallback so the developer experience is unchanged.
User Scenarios & Testing (mandatory)
User Story 1 - Images Load Directly from CDN (Priority: P1)
When a visitor views the image library or opens an image detail page, images and thumbnails are fetched directly from the CDN rather than through the application server. The page loads faster because image bytes no longer pass through the API.
Why this priority: Core value of the feature. Reduces API load and improves image load speed for all users.
Independent Test: Upload an image, open the library page, and inspect the network requests. Image and thumbnail requests should go directly to the CDN domain, not to /api/. The API response for the image list should include direct CDN URLs for each image and thumbnail.
Acceptance Scenarios:
- Given a published image, When the visitor loads the image library, Then each thumbnail
srcURL points to the CDN domain and loads without passing through the API - Given a published image, When the visitor opens the detail page, Then the full image
srcURL points to the CDN domain - Given the API returns image metadata, When the response is inspected, Then it includes a
file_urlandthumbnail_urlfield containing full CDN URLs
User Story 2 - Local Development Works Without CDN (Priority: P2)
In local development, where no CDN is configured, images continue to load via the existing API proxy endpoints. No additional setup is required to run the application locally.
Why this priority: Developer experience must not regress. The proxy endpoints must remain functional and be used automatically when no CDN is configured.
Independent Test: Run the application locally without setting a public base URL. Upload an image. Verify the library and detail pages load images correctly via the API proxy endpoints, with no errors or broken images.
Acceptance Scenarios:
- Given no CDN base URL is configured, When the API returns image metadata, Then
file_urlandthumbnail_urlpoint to the API proxy paths (e.g./api/v1/images/{id}/file) - Given no CDN base URL is configured, When a visitor views the library, Then thumbnails load via the API proxy with no broken images
- Given a CDN base URL is configured, When the application starts, Then all image URLs use the CDN domain instead of the proxy paths
Edge Cases
- What happens when the CDN base URL is set but the object does not exist in CDN storage? The browser receives a 404 from the CDN — the API does not re-proxy the content.
- What happens if an image has no thumbnail (thumbnail generation failed)? The
thumbnail_urlfield is absent or null; the UI falls back to the full image URL as it does today. - What happens if the CDN base URL has a trailing slash? The system normalises the URL to avoid double slashes in constructed paths.
Requirements (mandatory)
Functional Requirements
- FR-001: The API MUST include a
file_urlfield in all image metadata responses, containing the full URL from which the image file can be fetched - FR-002: The API MUST include a
thumbnail_urlfield in all image metadata responses when a thumbnail exists, containing the full URL from which the thumbnail can be fetched - FR-003: When a CDN base URL is configured,
file_urlandthumbnail_urlMUST point to the CDN domain - FR-004: When no CDN base URL is configured,
file_urlandthumbnail_urlMUST point to the existing API proxy endpoints so local development continues to work without additional setup - FR-005: The existing API proxy endpoints (
/images/{id}/file,/images/{id}/thumbnail) MUST remain functional regardless of whether a CDN base URL is configured - FR-006: The UI MUST use
file_urlandthumbnail_urlfrom the API response to render images, rather than constructing proxy URLs client-side - FR-007: The CDN base URL MUST be configurable via environment variable; no value is required in local development
- FR-008: A trailing slash in the configured CDN base URL MUST NOT result in double slashes in constructed image URLs
- FR-009: When
thumbnail_urlis null, the UI MUST fall back tofile_urlfor thumbnail display rather than rendering a broken image
Key Entities
- Image metadata response: Extended to include
file_urlandthumbnail_urlfields alongside existing fields (id,filename,tags,width,height,mime_type, etc.)
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: In production, zero image or thumbnail requests pass through the API server — all are served directly by the CDN
- SC-002: Local development requires no additional configuration beyond what is already required —
docker compose upcontinues to work with images loading correctly - SC-003: All existing image-related API integration tests continue to pass after the change
- SC-004: Image metadata responses include
file_urlandthumbnail_urlfields for 100% of images that have been successfully stored
Assumptions
- The CDN storage bucket and public domain are already configured and operational before this feature is deployed — this feature only changes how URLs are constructed and served, not how objects are stored
- Object keys in CDN storage are identical to those used in the existing storage backend — no key remapping is needed
- The CDN serves objects publicly without authentication — no signed URL generation is required
- The existing API proxy endpoints are retained as functional fallbacks; the UI stops calling them in production but they are not removed
- Local development uses the existing MinIO-backed proxy and does not require a locally running CDN