Skip to content

Media - Module Specification

1. Overview

1.1 Purpose

Media module manages photo and video metadata with URL resolution to Cloudflare R2 storage. Provides a centralized service for uploading, organizing, and serving media assets across all modules.

1.2 Scope

In Scope: - Media metadata storage (photos, videos) - R2 URL generation and resolution - Upload handling with validation - Cloudflare Images integration for on-the-fly transformations - Media organization via path prefixes

Out of Scope: - Actual file storage (handled by R2 directly) - Video transcoding (future consideration) - CDN configuration (infrastructure concern)


2. Dependencies

2.1 Upstream (What This Module Needs)

Module/Service What We Need Interface
Cloudflare R2 Object storage S3 API

Note: R2 bucket configuration via environment variables (infrastructure level)

2.2 Downstream (What Needs This Module)

Module/Service What They Need Interface
Inventory Puppy/breed images and videos Server Actions
CMS Blog/page images Server Actions
Customer Website Resolved media URLs for display Server Actions
Social Media Media for content pipeline Server Actions

3. Data Ownership

3.1 Entities This Module Owns

Entity Description Key Fields
Media Photo/video metadata id, type, filename, mimeType, size, r2Key, url

Note: Folder organization handled via structured r2Key path prefixes (e.g., puppies/{id}/, breeds/{slug}/, blog/{postId}/)

3.2 Entities This Module Uses (Read-Only)

Entity Owner How We Use It
User Auth Track who uploaded (uploadedBy)

3.3 Data Schema

import type { User } from '@/modules/auth';

type MediaType = 'image' | 'video';

interface Media {
  id: string;
  type: MediaType;
  filename: string;
  originalFilename: string;
  mimeType: string;
  size: number;                 // bytes
  r2Key: string;                // path in R2 bucket (e.g., puppies/123/photo1.jpg)
  url: string;                  // base Cloudflare Images URL

  // Video metadata (videos only)
  duration?: number;            // seconds

  width?: number;
  height?: number;
  alt?: string;                 // accessibility text
  uploadedBy: User['id'];
  createdAt: Date;
  updatedAt: Date;
}

// Pure utility function for URL transformations (no DB call)
function transformMediaUrl(baseUrl: string, options?: ImageTransformOptions): string;

interface ImageTransformOptions {
  width?: number;
  height?: number;
  fit?: 'contain' | 'cover' | 'crop' | 'scale-down';
  format?: 'webp' | 'avif' | 'auto';
  quality?: number;             // 1-100
}

4. Service Level Objectives

Objective Target Measurement
Upload Latency < 2s for images < 5MB Application monitoring
URL Resolution < 50ms Application monitoring
Availability 99.9% Health checks

Note: Image variants and format conversion handled on-the-fly by Cloudflare Images. No pre-generation needed.


5. Interface Contract

5.1 Internal Service Interface

interface MediaService {
  // Media operations
  getMedia(id: string): Promise<Media>;
  getMediaByIds(ids: string[]): Promise<Media[]>;
  listMedia(filters: MediaFilters, pagination: Pagination): Promise<PaginatedResult<Media>>;

  // Upload
  uploadMedia(file: File, options?: UploadOptions): Promise<Media>;
  uploadMediaFromUrl(url: string, options?: UploadOptions): Promise<Media>;

  // Update/Delete
  updateMedia(id: string, data: UpdateMediaDTO): Promise<Media>;
  deleteMedia(id: string): Promise<void>;
  bulkDeleteMedia(ids: string[]): Promise<void>;
}

interface MediaFilters {
  type?: MediaType;
  mimeType?: string;
  uploadedBy?: string;
  r2KeyPrefix?: string;         // Filter by path prefix
  search?: string;              // Search filename/alt
}

interface UploadOptions {
  r2KeyPrefix?: string;         // e.g., 'puppies/123/'
  alt?: string;
}

interface UpdateMediaDTO {
  alt?: string;
  filename?: string;
}

// Pure utility function (no DB call) - can be used anywhere
function transformMediaUrl(baseUrl: string, options?: ImageTransformOptions): string;

interface ImageTransformOptions {
  width?: number;
  height?: number;
  fit?: 'contain' | 'cover' | 'crop' | 'scale-down';
  format?: 'webp' | 'avif' | 'auto';
  quality?: number;             // 1-100
}

5.2 Remote Procedure Interface

Communication Pattern: Next.js Server Actions

Procedure Input Output Auth Description
getMedia { id } Media Staff+ Get media details
getMediaByIds { ids } Media[] Staff+ Get multiple media by IDs
listMedia MediaFilters PaginatedResult Staff+ List media with filters
uploadMedia File, UploadOptions Media Photographer+ Upload new media
updateMedia { id, data } Media Photographer+ Update media metadata
deleteMedia { id } void Admin Delete media

URL Transform: Use transformMediaUrl() utility with base media.url - no service call needed.


6. Error Handling Strategy

Error Code Condition User Message
MEDIA_NOT_FOUND Media doesn't exist "Media not found"
MEDIA_UPLOAD_FAILED R2 upload error "Upload failed, please try again"
MEDIA_INVALID_TYPE Unsupported file type "File type not supported"
MEDIA_TOO_LARGE File exceeds size limit "File is too large (max 50MB)"
MEDIA_INVALID_URL Invalid URL for URL upload "Invalid URL provided"
MEDIA_UNAUTHORIZED Insufficient permissions "You don't have permission to perform this action"

Error Response Format

interface ErrorResponse {
  code: string;
  message: string;
  details?: Record<string, unknown>;
}

7. Observability

7.1 Logging

Event Level Fields to Include
Media uploaded INFO media_id, type, size, r2_key, user_id
Media deleted INFO media_id, user_id
Upload failed ERROR error_code, filename, size, user_id
Bulk delete completed INFO count, user_id

7.2 Metrics

Metric Type Description
media_uploads_total Counter Total uploads by type
media_upload_size_bytes Histogram Upload size distribution
media_upload_duration_ms Histogram Upload latency
media_deletes_total Counter Total deletions

8. Testing Strategy

Test Type Coverage Target Tools Focus Areas
Unit 80%+ Vitest URL transformation, validation, filters
Integration Critical paths Vitest R2 upload/delete (mocked), metadata CRUD
E2E Happy paths Playwright Upload flow, media picker UI

9. Feature Inventory

Feature ID Name Status Priority
FEATURE-042 Media Upload Draft Must Have
FEATURE-043 Media CRUD Draft Must Have
FEATURE-044 URL Transform Utility Draft Must Have
FEATURE-045 Bulk Media Operations Draft Should Have
FEATURE-046 Media Picker Component Draft Must Have

10. Access Control

Operation Permissions

Operation Sales Kennel Staff Photographer Admin
View media Yes Yes Yes Yes
Upload media No No Yes Yes
Update metadata No No Yes Yes
Delete media No No No Yes
Bulk delete No No No Yes

Note: Photographer role is specific to media management.


11. Decisions

D1: Cloudflare Images for Transformations (2026-01-20)

Status: Accepted Context: Need responsive images with multiple sizes and formats Decision: Use Cloudflare Images for on-the-fly transformations instead of pre-generating variants Consequences: Simpler storage (one file), flexible sizing, automatic format optimization (webp/avif)

D2: Path-Based Organization (2026-01-20)

Status: Accepted Context: Need to organize media by usage context Decision: Use structured r2Key path prefixes instead of folder entities (e.g., puppies/{id}/, breeds/{slug}/) Consequences: Simpler schema, organization handled in code, easy to query by prefix

D3: Base URL with Transform Utility (2026-01-20)

Status: Accepted Context: Need flexible URL generation for different image sizes Decision: Store base URL in Media entity, use pure utility function for transformations Consequences: Simple service layer, utility usable anywhere (server/client), no extra DB calls for URL generation


12. References


Change Log

Version Date Author Changes
1.0 2026-01-20 Claude Initial module spec - Draft
1.1 2026-01-20 Claude Updated relationship types with explicit module imports (User['id'])