Image and thumbnail upload database flow
Last updated: 10 September 2026
1. Result
Images are uploaded directly to evequality/incoming/..., processed by an image Lambda with Sharp, written back to the shared bucket as responsive WebP plus a compatibility JPEG or PNG, verified and atomically published.
The primary compressed image key is stored in media_versions.object_key. media_assets.active_version_id selects it. For legacy video, audio and PDF thumbnails, publication also writes a full CDN URL to the relevant legacy column.
Video HLS processing does not extract a thumbnail automatically. The video file and its thumbnail are two separate assets, uploads, jobs and versions.
Normal images use Lambda. Oversized or timed-out work is safely rerouted to the scale-to-zero EC2 heavy worker, which runs the same Sharp processor.
This document describes the code and Terraform currently implemented in the repository; deployment and production validation are separate steps.
2. Supported image roles
| Legacy table | media_assets.role | media_assets.media_type | Publication target |
|---|---|---|---|
video | thumbnail | image | video.videoThumbnail full CDN URL |
audio | thumbnail | image | audio.audioThumbnail full CDN URL |
pdfSolutions | thumbnail | image | pdfSolutions.pdflogo full CDN URL |
bookCollection | cover | image | Modern active version only; the current publisher does not rewrite legacy book_image BLOB |
The stable identity is always legacy table + legacy ID + role. A video content asset and video thumbnail asset can therefore advance independently without overwriting each other's active version.
3. Main AWS and database flow
4. Stage-by-stage database changes
Stage A — authorize image upload
The administrator selects the exact image asset and uploads jpg, jpeg, png or webp. The application checks the legacy permission corresponding to that asset and inserts:
| Table | Fields written |
|---|---|
media_uploads | asset/admin/revision identity, bucket and incoming key, original filename, expected size/MIME/checksum, optional multipart fields, status = authorized, expiry |
admin_audit_events | media_upload.authorized |
Example temporary object:
1incoming/image/{media_asset_ulid}/revision-{number}/{upload_ulid}.pngThe browser sends bytes directly to S3 through signed requests. Laravel is control-plane only.
Stage B — confirm and create image job
Laravel verifies S3 metadata, exact byte count, checksum and actual image signature before it accepts the upload.
| Table | Transition or write |
|---|---|
media_uploads | authorized -> uploaded -> confirmed; stores confirmation and S3 version data |
media_assets | advances source_revision |
media_jobs | inserts media_type = image, profile_version = image-v1, processor = image_lambda, status = pending, generation 1, default three attempts |
outbox_messages | inserts pending media.job.requested for the image queue |
admin_audit_events | records media_upload.confirmed with job ID |
The asset/revision/profile idempotency key prevents duplicate jobs when confirmation is repeated.
Stage C — outbox and image queue
The scheduled dispatcher performs:
1outbox_messages: pending -> dispatching -> dispatched
2media_jobs: pending -> dispatchedThe image queue uses SQS-managed encryption, 20-second long polling, 180-second visibility, four-day message retention and redrive after three receives to a DLQ retained for 14 days.
Its Lambda event-source mapping has batch size one and partial-batch failure reporting.
Stage D — image Lambda lease
The ARM64 image Lambda has:
| Setting | Value |
|---|---|
| Reserved concurrency | 1 |
| Memory | 1,024 MiB |
| Ephemeral storage | 1,024 MiB |
| Function/application timeout | 120 seconds |
| Default source-size limit | 25 MiB |
| Maximum decoded pixels | 60,000,000 |
| Requested responsive widths | 320, 800 and 1,600 px |
Claiming work changes:
| Table | Transition or write |
|---|---|
media_job_attempts | inserts processor = image_lambda, attempt number, status = claimed, lease identity and timestamps |
media_jobs | increments attempts and changes dispatched -> processing |
media_job_attempts | claimed -> processing |
At least three times the source size, with a 100 MB minimum, must be free in the temporary work directory.
Stage E — Sharp optimization
The processor auto-rotates the image, never enlarges it, respects the 60-million-pixel guard and generates:
- WebP quality 82 at configured widths below the source width, plus the source width capped at 1,600 px;
- one compatibility image at the largest allowed width;
- PNG compatibility when transparency exists, otherwise mozjpeg JPEG quality 85.
Generic image filenames are:
1w320.webp
2w800.webp
3w1600.webp
4compat.jpg # or compat.png for transparencyThe largest generated WebP becomes media_versions.object_key. There must be a primary WebP and at least two total variants.
For a video thumbnail, the same data is named for legacy compatibility and placed beside the video's HLS files:
1cbsc12/.../video-name-code/
2├── master.m3u8
3├── 240p/...
4├── thumbnail.webp
5├── thumbnail-w320.webp
6├── thumbnail-w800.webp
7└── thumbnail-compat.jpgThe largest WebP is exactly thumbnail.webp. Audio and PDF thumbnails use their EVE-compatible audio/... or pdf/... catalogue prefix with the generic w*.webp names. A book cover, whose legacy row is not handled by the catalogue path builder, uses media/image/{asset_ulid}/....
For replacement versions, versions/r{revision} is added when that asset already has a version.
The verifier checks every stored variant key, non-empty object size, allowed WebP/JPEG/PNG signature, valid width/height and a minimum of two variants.
Stage F — atomic publication
After verification, a transaction rejects stale generation/revision/lease state, cancellation or a blocked asset, and then commits:
| Table | Fields or transition |
|---|---|
media_jobs | processing -> publish_pending -> published |
media_versions | inserts format = webp, processor, primary object_key, common prefix, total bytes, source dimensions/transparency/full variant inventory, verified/published timestamps |
media_assets | sets active_version_id |
| Legacy table | video thumbnail sets videoThumbnail; audio thumbnail sets audioThumbnail; PDF thumbnail sets pdflogo |
media_job_attempts | processing -> succeeded; stores completion |
publication_audits | records previous/new version, job, attempt, generation and actor |
For example:
1media_versions.object_key = cbsc12/.../video-name-code/thumbnail.webp
2video.videoThumbnail = https://evevideoquality.rayandmartin.in/cbsc12/.../video-name-code/thumbnail.webpThe final modern key is relative to the shared S3 bucket. The compatibility legacy field is a full CDN URL.
For bookCollection/cover, the current transaction still publishes media_versions and changes media_assets.active_version_id, but it does not replace bookCollection.book_image. Any migration from the legacy BLOB to modern cover delivery needs an explicit reader/compatibility change before the BLOB can be retired.
Important media_versions fields
| Field | Meaning |
|---|---|
object_key | Primary relative S3 key for the largest WebP |
object_prefix | Directory shared by all image variants |
metadata.object_keys | Complete verified responsive/compatibility object inventory |
metadata.variants | Key, format, width and height for every variant |
file_size_bytes | Sum of all generated variant sizes |
media_job_attempt_id | Producing Lambda/EC2 attempt relation, not an image URL |
inventory_object_key | Currently null; inventory is embedded in metadata.object_keys |
5. Database relationship for thumbnails
Prisma model names differ in capitalization, but the physical MySQL tables are video, audio, pdfSolutions, media_assets, media_uploads, media_jobs, media_job_attempts, outbox_messages, media_versions and publication_audits.
6. Image status timeline
When confirmation verification fails, Laravel clears confirmation_started_at and returns the upload to authorized, allowing another confirmation attempt before expiry.
7. Heavy-worker fallback
When the source exceeds the default 25 MiB Lambda limit or the Sharp command times out:
| Table | Change |
|---|---|
media_job_attempts | current Lambda attempt becomes failed with heavy_fallback and reason |
media_jobs | processor = heavy_worker, status = pending, generation increases, fallback reason and new retry budget recorded |
outbox_messages | a fresh fallback message is created for the video/heavy SQS queue |
The conversion ASG starts from zero to one EC2 worker. That worker claims a new attempt and invokes the same ImageProcessor/Sharp script with the long heavy-media timeout. The application EC2 never performs image compression.
8. Failures, retry, rollback and cleanup
- Decode warnings, pixel-limit violations, missing variants, invalid signatures/dimensions, storage failures and process failures become attempt error codes.
- Retryable failures change
media_jobsback todispatchedwith exponential delay; exhausted jobs becomefailed. - The SQS message is retried and ultimately redriven to the media-specific DLQ after three receives.
- Uploaded candidate variants from a failed attempt are deleted best-effort.
- Expired database leases are recovered every minute.
- Manual retry creates a new generation and outbox row; stale messages cannot publish.
- Rollback revalidates the complete stored variant inventory, changes the active version, refreshes the compatible legacy URL where supported and writes
publication_audits. - The hourly source purger removes only the matching
incoming/image/...source after the active version is verified and published for the default 24-hour retention period. - Purge claim, success and failure are recorded in
media_uploads.source_delete_started_at,source_deleted_atandsource_delete_error_code.
9. Final table ownership
| Table | Image responsibility |
|---|---|
Legacy video/audio/pdfSolutions | Catalogue row and backward-compatible thumbnail URL |
media_assets | Stable thumbnail/cover identity and active version pointer |
media_uploads | Temporary original image and integrity contract |
media_jobs | Image Lambda or heavy-worker state |
media_job_attempts | Per-attempt lease, owner and failure/success evidence |
outbox_messages | Durable image/heavy queue dispatch |
media_versions | Primary key plus complete responsive variant inventory |
publication_audits | Activation and rollback history |
admin_audit_events | Administrator upload actions |