Media Storage And Delivery
Wavemap’s media architecture is artist-first today, but the boundaries are intentionally shaped so other media-bearing entities can adopt the same model later.
Use this page when changing artist media records, media upload or sync flows, storage adapter selection, local media execution modes, image ingest, media delivery fields, or deployed-dev media validation.
Use Media Workflow And Validation for the practical lane choice between mocked storage, local emulators, deployed-dev media proof, browser media smoke, and discrepancy reporting.
Current Scope
Section titled “Current Scope”The current media platform is intentionally bounded:
- Artist images only.
- Server-mediated uploads through backend media endpoints.
- Public render URLs returned by the backend.
- One active storage locator per asset.
- Canonical image plus a first-pass thumbnail and blur preview derivative.
- AWS S3 as the primary serious-validation path.
- Azure retained as a continuity path, not the roadmap driver.
- Mocked storage and provider emulators for local and test confidence.
The first pass does not try to solve cross-entity media, video/audio processing, direct-to-storage uploads, signed URLs, private delivery, multi-locator replicas, or a broad variant matrix.
Core Decisions
Section titled “Core Decisions”- Keep frontend upload draft state separate from persisted media records.
- Keep
createArtistandeditArtistfocused on JSON artist details. - Route real media writes through dedicated artist-media workflows.
- At some later point, explore the possibility of a process of writing media directly to storage, bypassing proxying it through the application server.
- Return render-ready media fields such as
profileImageURL,media[].src, and thumbnail/preview fields to the frontend. - Store provider-agnostic storage locator fields as canonical identity rather than provider-shaped URLs.
- Keep media kind, purpose, and lifecycle status as separate concepts.
- Extend the existing
artist_mediatable boundary before introducing broader media tables. - Keep storage provider, execution target, and application environment as separate runtime concepts.
- Use the active write provider for new uploads, but use each stored locator’s provider for reads, deletes, and cleanup.
- Keep media ingest behind a dedicated backend seam so validation, normalization, and derivative generation can evolve without changing upload transport or frontend reads.
Vocabulary
Section titled “Vocabulary”| Term | Meaning |
|---|---|
| Media draft | Temporary frontend upload-selection state before an asset is persisted. |
| Media asset | The logical persisted media record associated with an entity. |
| Storage locator | Provider-agnostic identity for the underlying stored object. |
| Render URL | The URL the backend returns for frontend display. |
| Kind | Technical media category, such as image, video, or audio. |
| Purpose | Product meaning, such as profile, gallery, clip, or track. |
| Status | Lifecycle state, such as pending, processing, ready, or failed. |
| Artifact kind | Stored object meaning inside the media pipeline, such as canonical, thumbnail, or poster. |
| Execution target | Runtime target for a provider implementation, such as cloud, emulator, or mocked. |
Layer Boundaries
Section titled “Layer Boundaries”| Layer | Owns |
|---|---|
| Frontend draft layer | Temporary selected files, browser previews, form ordering, and UI workflow state before submission. |
| Frontend workflow layer | Sequencing artist creation/update, upload calls, sync calls, retry posture, and UI-facing workflow state. |
| Backend media routes | Application-facing media write contracts, request validation, and route-level response envelopes. |
| Image ingest layer | File inspection, validation, normalization, metadata policy, and derivative generation. |
| Database layer | Canonical metadata, lifecycle status, sort order, purpose, and storage locator fields. |
| Storage adapter layer | Provider-specific upload, delete, public URL resolution, legacy URL parsing, and emulator support. |
| Frontend read layer | Render-ready media fields and layout metadata, not provider-specific storage details. |
Endpoint hooks should stay close to individual API routes. Feature-level orchestration hooks should own sequencing, diffing, retry posture, and UI-facing workflow state.
Canonical Record Shape
Section titled “Canonical Record Shape”The first-pass media record is shaped around canonical metadata plus locator identity:
type TArtistMediaRecord = { ID: string artistID: string kind: "image" | "video" | "audio" purpose: "profile" | "gallery" | "clip" | "track" status: "pending" | "processing" | "ready" | "failed" originalFileName: string mimeType: string byteSize: number width?: number height?: number durationMs?: number altText?: string sortOrder: number storageProvider: "azure" | "s3" | "mocked" storageLocation: string storageKey: string thumbnailStorageProvider?: "azure" | "s3" | "mocked" thumbnailStorageLocation?: string thumbnailStorageKey?: string thumbnailURL?: string blurDataURL?: string}The exact implementation can evolve, but these boundaries should remain stable:
- The record stores canonical metadata and locator identity, not a provider URL as the source of truth.
purposecarries product meaning instead of a narrow flag likeisProfileImage.sortOrderdescribes persisted collection order, not upload-batch order.- Thumbnail locator fields follow the same provider-agnostic direction as canonical media.
- Transitional read conveniences can remain while the canonical locator model settles.
Transitional Simplicity
Section titled “Transitional Simplicity”The first pass should keep existing product flows stable while the canonical media model settles.
Allowed transition posture:
- Keep returning
profileImageURLandmedia[].srcas read conveniences so frontend surfaces do not need broad churn. - Keep
profileImageURLderived from persisted profile-purpose media rather than restoring an artist-table URL as source of truth. - Keep legacy URL fallback and legacy URL parsing inside the backend/storage boundary while older rows still require it.
- Keep
mediaTypeIDas the active database representation of media kind while additive canonical fields land. - Keep the first real upload slice focused on artist images.
- Keep the create flow simple: create the artist, then attach artist media through the dedicated media workflow.
- Keep the edit flow bounded: patch artist details, then sync media collection state through a media-focused request.
- Keep one backend storage utility/adapter layer small enough to own upload, delete, public URL resolution, and legacy URL parsing before expanding into a broader provider plugin system.
Do not turn the transition into a second source of truth. Transitional fields and fallbacks are there to preserve product behavior while canonical locators take over.
Storage Runtime Model
Section titled “Storage Runtime Model”Media runtime selection separates three concerns:
| Concern | Examples | Persistence Boundary |
|---|---|---|
| Storage provider | s3, azure, mocked | Stored on media locators because it identifies how to read/delete the stored object. |
| Execution target | cloud, emulator, mocked | Runtime config only. Do not persist emulator names as providers. |
| Application environment | development, test, dev | Runtime/deploy context. Use it to reject unsafe combinations. |
Emulators are execution targets of real provider adapters:
- LocalStack is the emulator target for S3-shaped work.
- Azurite is the emulator target for Azure-shaped continuity work.
- Neither LocalStack nor Azurite should appear as persisted
storageProvidervalues.
Mocked storage is the deliberate exception. It is a first-class storage double for development and test app logic, so
locators created by the mocked adapter can honestly use storageProvider: "mocked" rather than pretending to be S3 or
Azure.
Execution Modes
Section titled “Execution Modes”| Mode | Use For | Avoid For |
|---|---|---|
| Mocked storage | Day-to-day app logic, unit tests, most request tests, non-storage work. | Provider behavior, object-store path construction, IAM, bucket policy, public URLs. |
| Emulator-backed S3 | Local AWS-shaped adapter behavior through LocalStack. | Real IAM, CloudFront, or deployed-dev proof. |
| Azure emulator | Azure continuity smoke through Azurite. | Driving the AWS-first roadmap. |
Real AWS dev media | Serious end-to-end validation, IAM, bucket policy, S3/CloudFront proof. | Ordinary local work or broad reset scripts. |
| Future production | User-facing durable media. | Disposable reset assumptions from deployed dev. |
Local reset behavior must follow the same split:
- Mocked mode has no durable provider state.
- Emulator mode can reset local emulator media state intentionally.
- Cloud mode must never be targeted by generic local reset commands.
The deployed dev environment uses real S3 storage behind the app delivery path. See
Deployed Dev Environment and Deployed Dev Lifecycle
for the operator-facing lifecycle boundary.
Adapter Rules
Section titled “Adapter Rules”The storage adapter contract should stay provider-neutral above provider-specific modules.
Adapters own:
- Uploading stored objects.
- Deleting stored objects.
- Resolving public render URLs.
- Parsing legacy provider URLs where migration needs it.
- Supporting
cloudoremulatorexecution targets inside the provider implementation.
Application media code owns:
- Product meanings such as profile, gallery, canonical, thumbnail, and blur preview.
- Entity/purpose/kind/sort metadata.
- Choosing the active write adapter from runtime config.
- Resolving read/delete behavior from the persisted locator’s provider.
This split enables gradual provider migration. New uploads can move to S3 while older assets still resolve from Azure locators, because reads and deletes follow the stored locator rather than the current active write provider.
URL And Delivery Boundaries
Section titled “URL And Delivery Boundaries”Frontend reads should receive render-ready values, not storage-provider internals.
Current public read fields may include:
srcfor full-display media.thumbnailSrcorthumbnailURLfor small surfaces.blurDataURLfor tiny preview or blurred placeholder presentation.widthandheightwhen the backend can provide trustworthy dimensions.
Surface rules:
- Showcase, gallery, card, search-result, and table surfaces should prefer thumbnail fields.
- Carousel and large-detail surfaces should use the full-display
src. - Blurred background treatments should prefer a tiny preview or blur payload rather than loading the full image again.
- Only the immediately important above-the-fold asset should be eager by default.
A backend-owned media resolution route remains deferred. Direct render URLs are still the simplest current shape, but the frontend contract should not require provider-specific URL knowledge.
Image Ingest Policy
Section titled “Image Ingest Policy”The backend image-ingest seam sits before storage finalization.
Current first-pass posture:
- Validate non-empty file presence.
- Enforce supported MIME types.
- Decode the image and verify the decoded format matches the upload contract.
- Reject uploads over the hard request byte ceiling.
- Reject images over the hard decoded-dimension ceiling.
- Downscale valid-but-large images to the canonical delivery target.
- Auto-orient before stripping metadata so render orientation does not depend on EXIF.
- Re-encode to the same allowed output format.
- Strip embedded metadata from public-facing outputs through Sharp defaults.
- Persist canonical metadata for the stored canonical asset.
- Generate a stored thumbnail and an inline blur preview from the normalized canonical image.
Client-side preflight is advisory. Server-side validation is authoritative.
The upload transport, canonical media record, and frontend read contract should stay stable while validation, normalization, thumbnail generation, preview generation, and future async processing evolve behind the ingest seam.
Lifecycle And Reconciliation
Section titled “Lifecycle And Reconciliation”The first-pass artist-image workflow is storage-first and inserts media rows as ready only after required storage work
succeeds.
Current rule:
- Validate and ingest the uploaded image synchronously.
- Upload the canonical asset and required first-pass derivatives.
- Insert the media row as
ready. - If storage fails before the row exists, return failure without creating a media row.
- If DB finalization fails after request-owned provider objects were created, attempt compensating deletes.
The richer pending / processing / failed lifecycle remains reserved for future flows where the application
intentionally creates non-ready rows, such as upload sessions, async processing, moderation, malware scanning, video
transcoding, or retryable finalization.
Until non-ready rows exist intentionally, stale pending / failed reporting is lower signal. The useful current
operator visibility is DB/S3 discrepancy reporting plus best-effort request-local cleanup.
Deployed-Dev Boundary
Section titled “Deployed-Dev Boundary”In deployed dev:
- The media bucket is private.
- Browser-facing media is served through the deployed app CloudFront distribution under
/media/*, not through direct public bucket access. - Runtime media access belongs to the runtime host role, not frontend/browser IAM credentials.
- Runtime media config should flow through deployed-dev runtime config, SSM, and host-side env rendering.
- GitHub Actions should not decrypt app runtime secrets or own media runtime credentials.
- Database reset does not delete S3 media objects and can create application-orphaned media.
- Media bucket replacement or stack teardown can delete media objects because the deployed-dev bucket is disposable at the infrastructure lifecycle level.
Use Configuration And Secrets when adding or changing runtime media config. Use Runbooks for the read-only deployed-dev discrepancy report and Media Bucket Replacement before any deployed-dev bucket replacement or destroy.
Deliberate Deferrals
Section titled “Deliberate Deferrals”Keep these out of the current architecture unless a concrete product or operational need appears:
- Cross-entity media generalization.
- Direct-to-storage upload sessions.
- Signed URL or private-delivery complexity.
- Multi-locator replicas for the same asset.
- Video/audio upload and processing workflows.
- Broad responsive variant matrices.
- Crop-intent modeling or crop-aware derivative generation.
- Automated repair or scheduled cleanup before report data justifies it.
- Local emulator orchestration through Pulumi.