Skip to content

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.

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.

  • Keep frontend upload draft state separate from persisted media records.
  • Keep createArtist and editArtist focused 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_media table 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.
TermMeaning
Media draftTemporary frontend upload-selection state before an asset is persisted.
Media assetThe logical persisted media record associated with an entity.
Storage locatorProvider-agnostic identity for the underlying stored object.
Render URLThe URL the backend returns for frontend display.
KindTechnical media category, such as image, video, or audio.
PurposeProduct meaning, such as profile, gallery, clip, or track.
StatusLifecycle state, such as pending, processing, ready, or failed.
Artifact kindStored object meaning inside the media pipeline, such as canonical, thumbnail, or poster.
Execution targetRuntime target for a provider implementation, such as cloud, emulator, or mocked.
LayerOwns
Frontend draft layerTemporary selected files, browser previews, form ordering, and UI workflow state before submission.
Frontend workflow layerSequencing artist creation/update, upload calls, sync calls, retry posture, and UI-facing workflow state.
Backend media routesApplication-facing media write contracts, request validation, and route-level response envelopes.
Image ingest layerFile inspection, validation, normalization, metadata policy, and derivative generation.
Database layerCanonical metadata, lifecycle status, sort order, purpose, and storage locator fields.
Storage adapter layerProvider-specific upload, delete, public URL resolution, legacy URL parsing, and emulator support.
Frontend read layerRender-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.

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.
  • purpose carries product meaning instead of a narrow flag like isProfileImage.
  • sortOrder describes 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.

The first pass should keep existing product flows stable while the canonical media model settles.

Allowed transition posture:

  • Keep returning profileImageURL and media[].src as read conveniences so frontend surfaces do not need broad churn.
  • Keep profileImageURL derived 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 mediaTypeID as 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.

Media runtime selection separates three concerns:

ConcernExamplesPersistence Boundary
Storage providers3, azure, mockedStored on media locators because it identifies how to read/delete the stored object.
Execution targetcloud, emulator, mockedRuntime config only. Do not persist emulator names as providers.
Application environmentdevelopment, test, devRuntime/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 storageProvider values.

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.

ModeUse ForAvoid For
Mocked storageDay-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 S3Local AWS-shaped adapter behavior through LocalStack.Real IAM, CloudFront, or deployed-dev proof.
Azure emulatorAzure continuity smoke through Azurite.Driving the AWS-first roadmap.
Real AWS dev mediaSerious end-to-end validation, IAM, bucket policy, S3/CloudFront proof.Ordinary local work or broad reset scripts.
Future productionUser-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.

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 cloud or emulator execution 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.

Frontend reads should receive render-ready values, not storage-provider internals.

Current public read fields may include:

  • src for full-display media.
  • thumbnailSrc or thumbnailURL for small surfaces.
  • blurDataURL for tiny preview or blurred placeholder presentation.
  • width and height when 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.

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.

The first-pass artist-image workflow is storage-first and inserts media rows as ready only after required storage work succeeds.

Current rule:

  1. Validate and ingest the uploaded image synchronously.
  2. Upload the canonical asset and required first-pass derivatives.
  3. Insert the media row as ready.
  4. If storage fails before the row exists, return failure without creating a media row.
  5. 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.

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.

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.