[Perf][ArtStudio] Serve artifact images via short-TTL presigned MinIO URLs + HTTP caching — stop streaming every image through the API (follow-up to #783, epic #413)
#784
Observed in prod (2026-07-21): Art Studio images load from https://api.spikersoft.com/api/artstudio/{id}/artifacts/{gridFsId}/download — every thumbnail/preview streams through the API, and because the Angular side fetches blobs via the authenticated HttpClient (createObjectURL), the browser HTTP cache never engages. Zero caching, API bandwidth+CPU on every image view. #783 moved the STORE to MinIO but the SERVING path still proxies everything.
Design (house pattern — book covers, BookController.ToDtoWithCover, #618/#719)
Backend
ArtAssetArtifactRef gains a transient [BsonIgnore] string? Url — a short-TTL (1h) presigned MinIO GET URL minted per response, after any cache layer, in ArtStudioController via the already-registered keyed IObjectStore("art-asset-artifacts") (null in local dev → field stays null → client falls back to the authenticated download routes; GridFS local-dev path unaffected).
Applied on: owner detail + owner list, gallery detail + gallery list (the projections embed ArtAssetArtifactRef, so the field flows through).
Presigned URLs carry response-cache-control: private, max-age=3600 via ResponseHeaderOverrides (new optional param on IObjectStore.GetPresignedUrl).
The API download routes gain Cache-Control: private, max-age=86400, immutable (artifact content per id is immutable) so even the fallback/blob path caches.
Prereq verified: API S3 client signs against public minio.spikersoft.com (documented in the stack file precisely for presigned book covers) and api-svc got rw on the art buckets in infra #138.
Angular
ArtAssetArtifactRef model gains url?: string | null; image display paths (shell thumbnails, gallery cards, moderation previews, Photo Stack preview, Touch-Up frame strip) prefer artifact.url in img src and skip the blob fetch entirely.
Bearer interceptor is URL-pattern-scoped, so presigned hosts never get an Authorization header (no S3 double-auth clash).
Deliberately OUT of scope
Games-handoff manifest URLs stay API download routes — the manifest is an explicit breaking-sensitive contract whose consumers fetch with a bearer token.
GLB/mesh viewer + file downloads stay on the authenticated blob path (GLTFLoader fetch needs CORS on MinIO — separate verification; cross-origin download attribute is ignored by browsers).
Observed in prod (2026-07-21): Art Studio images load from `https://api.spikersoft.com/api/artstudio/{id}/artifacts/{gridFsId}/download` — every thumbnail/preview streams through the API, and because the Angular side fetches blobs via the authenticated HttpClient (`createObjectURL`), the browser HTTP cache never engages. Zero caching, API bandwidth+CPU on every image view. #783 moved the STORE to MinIO but the SERVING path still proxies everything.
## Design (house pattern — book covers, `BookController.ToDtoWithCover`, #618/#719)
**Backend**
- `ArtAssetArtifactRef` gains a transient `[BsonIgnore] string? Url` — a short-TTL (1h) presigned MinIO GET URL minted **per response, after any cache layer**, in `ArtStudioController` via the already-registered keyed `IObjectStore("art-asset-artifacts")` (null in local dev → field stays null → client falls back to the authenticated download routes; GridFS local-dev path unaffected).
- Applied on: owner detail + owner list, gallery detail + gallery list (the projections embed `ArtAssetArtifactRef`, so the field flows through).
- Presigned URLs carry `response-cache-control: private, max-age=3600` via `ResponseHeaderOverrides` (new optional param on `IObjectStore.GetPresignedUrl`).
- The API download routes gain `Cache-Control: private, max-age=86400, immutable` (artifact content per id is immutable) so even the fallback/blob path caches.
- Prereq verified: API S3 client signs against public `minio.spikersoft.com` (documented in the stack file precisely for presigned book covers) and `api-svc` got rw on the art buckets in infra #138.
**Angular**
- `ArtAssetArtifactRef` model gains `url?: string | null`; image display paths (shell thumbnails, gallery cards, moderation previews, Photo Stack preview, Touch-Up frame strip) prefer `artifact.url` in `img src` and skip the blob fetch entirely.
- Bearer interceptor is URL-pattern-scoped, so presigned hosts never get an Authorization header (no S3 double-auth clash).
## Deliberately OUT of scope
- **Games-handoff manifest URLs** stay API download routes — the manifest is an explicit breaking-sensitive contract whose consumers fetch with a bearer token.
- **GLB/mesh viewer + file downloads** stay on the authenticated blob path (GLTFLoader `fetch` needs CORS on MinIO — separate verification; cross-origin `download` attribute is ignored by browsers).
- **Quarantine previews** stay authenticated-only (safety-flagged images, staff review).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Observed in prod (2026-07-21): Art Studio images load from
https://api.spikersoft.com/api/artstudio/{id}/artifacts/{gridFsId}/download— every thumbnail/preview streams through the API, and because the Angular side fetches blobs via the authenticated HttpClient (createObjectURL), the browser HTTP cache never engages. Zero caching, API bandwidth+CPU on every image view. #783 moved the STORE to MinIO but the SERVING path still proxies everything.Design (house pattern — book covers,
BookController.ToDtoWithCover, #618/#719)Backend
ArtAssetArtifactRefgains a transient[BsonIgnore] string? Url— a short-TTL (1h) presigned MinIO GET URL minted per response, after any cache layer, inArtStudioControllervia the already-registered keyedIObjectStore("art-asset-artifacts")(null in local dev → field stays null → client falls back to the authenticated download routes; GridFS local-dev path unaffected).ArtAssetArtifactRef, so the field flows through).response-cache-control: private, max-age=3600viaResponseHeaderOverrides(new optional param onIObjectStore.GetPresignedUrl).Cache-Control: private, max-age=86400, immutable(artifact content per id is immutable) so even the fallback/blob path caches.minio.spikersoft.com(documented in the stack file precisely for presigned book covers) andapi-svcgot rw on the art buckets in infra #138.Angular
ArtAssetArtifactRefmodel gainsurl?: string | null; image display paths (shell thumbnails, gallery cards, moderation previews, Photo Stack preview, Touch-Up frame strip) preferartifact.urlinimg srcand skip the blob fetch entirely.Deliberately OUT of scope
fetchneeds CORS on MinIO — separate verification; cross-origindownloadattribute is ignored by browsers).Resolved — both halves merged to master:
[BsonIgnore] UrlonArtAssetArtifactRef, minted per response post-authorization via the keyedart-asset-artifactsstore (1h TTL, book-cover pattern) on owner detail/list, gallery detail/list, submissions + flagged;IObjectStore.GetPresignedUrlgained aresponseCacheControloverride (MinIO servesprivate, max-age=3600); API download routes now sendCache-Control: private, max-age=86400, immutable. 119 targeted tests green.artifact.urlas plainimg srcwith blob-fetch fallback for local dev. GLB loads, file downloads, and touch-up canvas frames deliberately stay on the authenticated blob path; manifest contract and quarantine untouched (documented out-of-scope above). 31/31 spec files green.Deploys ride the normal master-push pipelines. Closing.