Central media table, proxy endpoint, and upload helpers for all file storage
All file uploads across the system go through a media system that:
media tablephoto-a3f2.jpg) as the public reference instead of raw S3 URLsThis keeps S3 bucket details private, allows future storage migration, and provides a single place to manage media lifecycle (soft delete, etc.).
Location: packages/db/src/schema/media.ts
| Column | Type | Description |
|---|---|---|
id | text (PK) | nanoid |
filename | text (unique) | Public reference: {originalBase}-{4char}.{ext} |
originalFilename | text | Original file name |
s3Key | text | S3 object key |
s3Bucket | text | S3 bucket name |
s3Region | text | AWS region |
contentType | text | MIME type |
status | enum | UPLOADING, COMPLETE, SOFT_DELETE, DELETE |
createdAt, updatedAt | timestamp | Audit fields |
Filename format: {base}-{suffix}.{ext} — e.g., headshot_studio-a3f2.jpg. The 4-character suffix (from nanoid) ensures uniqueness.
Location: packages/db/src/access/media.ts
| Function | Purpose |
|---|---|
createMedia(data) | Insert new media record |
getMediaByFilename(filename) | Lookup by unique filename (used by proxy) |
getMediaById(id) | Lookup by primary key |
updateMediaStatus(id, status) | Transition status (e.g., to SOFT_DELETE) |
Location: packages/util-srv/src/s3/media-upload.ts
Import: import { uploadMediaFile, uploadMediaFromUrl, getMediaUrl } from "@zooly/util-srv"
uploadMediaFile(buffer, originalFilename, contentType, basePath)Uploads a buffer to S3 and returns metadata for the caller to persist in the media table.
{ filename, s3Key, s3Bucket, s3Region, contentType }{basePath}/{filename} (e.g., likeness-image-assets/photo-a3f2.jpg)createMedia()uploadMediaFromUrl(sourceUrl, basePath, defaultContentType?, defaultExtension?)Fetches content from a URL, then delegates to uploadMediaFile. Used for social scraping (profile images) and URL-based asset imports.
getMediaUrl(filename)Builds the proxy URL for a filename:
{NEXT_PUBLIC_APP_URL}/api/media/{filename}
Example: https://app.example.com/api/media/photo-a3f2.jpg
Endpoint: GET /api/media/[filename]
Location: apps/zooly-app/app/api/media/[filename]/route.ts
Authentication: None (media URLs are public, same as S3 was)
Behavior:
filename from route paramsgetMediaByFilename(filename)SOFT_DELETE or DELETE → 404UPLOADING → 202 (file still uploading)getS3Object(s3Key, s3Bucket)Response with Content-Type and Cache-Control: public, max-age=31536000, immutableAsset uploads use the media system as follows:
addImageAssets → uploadMediaFile → createMedia → store filename as contentUrl in likeness_assetsaddVoiceAssets → same pattern with basePath: "likeness-voice-assets"scrapeSocial → uploadMediaFromUrl → createMedia → store filename as account.imageUrl and contentUrlcreateVoiceSample → uploadMediaFile → store filename as voiceSampleUrl and voiceExampleWhen returning data to clients (e.g., in search results or list endpoints), filenames are mapped through getMediaUrl() so the UI receives proxy URLs, never raw S3 URLs.