This document describes the image subsystem built on top of src/model/image.json, src/core/image.py, and the public delivery component in src/component/cmp_5300_image/README.md.
It started as a database-model note, but it now documents the broader contract that the rest of the application relies on.
This document covers:
Images are stored in the image table as generated WebP variants:
thumbImgmediumImgfullImgThe subsystem also uses a second table, image_disabled, to track logical image state such as:
deletedmoderatedspamThe general rule is:
image stores the immutable asset payloadimage_disabled stores mutable state about whether that image should be visible or blockedImages are immutable by design. The image table does not include a modified column.
If the content of an image changes, the application should create a new imageId and keep caches tied to the old immutable asset id.
profileId is a logical reference to the owning profile, but it is not enforced as a foreign key to the profile table.
This is intentional:
Important distinction:
image.profileId to the profile tableimage_disabled.imageId to image.imageIdThe subsystem is designed around logical deletion first.
When a user or admin deletes an image, the first step is not physical removal from image. Instead, the image is marked in image_disabled with reason deleted.
That gives the system a trash/paper-bin stage:
deleted reasonPhysical deletion is expected to happen later in a separate cleanup task.
Target sizes are controlled by runtime configuration, not by database schema.
Typical settings:
IMAGE_THUMB_SIZE=100
IMAGE_MEDIUM_WIDTH=420
IMAGE_FULL_WIDTH=1920
Generation rules:
thumb is square and generated with crop-fit semanticsmedium is resized proportionally to a configured max widthfull is resized proportionally only when wider than the configured max widthThe database stores:
imageThe immutable image asset table.
| Field | Portable type | Description |
|---|---|---|
imageId |
VARCHAR(64) |
Primary key for the immutable asset. |
profileId |
VARCHAR(64) |
Logical owner profile id. No FK to profile storage. |
albumCode |
VARCHAR(64) |
Album/group code inside the profile scope. |
thumbImg |
BLOB |
Square thumbnail WebP binary. |
mediumImg |
BLOB |
Medium WebP binary. |
fullImg |
BLOB |
Full WebP binary. |
thumbWidth |
INT |
Thumbnail width. |
thumbHeight |
INT |
Thumbnail height. |
mediumWidth |
INT |
Medium width. |
mediumHeight |
INT |
Medium height. |
fullWidth |
INT |
Full width. |
fullHeight |
INT |
Full height. |
thumbBytes |
INT |
Thumbnail byte size. |
mediumBytes |
INT |
Medium byte size. |
fullBytes |
INT |
Full byte size. |
created |
INT(11) |
Creation timestamp. |
image_disabledMutable state table for hidden, moderated, spam, or deleted images.
| Field | Portable type | Description |
|---|---|---|
reason |
INT |
Disabled reason code from configuration. |
imageId |
VARCHAR(64) |
FK to image.imageId. |
description |
VARCHAR(256) |
Optional explanation. |
modified |
BIGINT |
Last change timestamp. |
created |
BIGINT |
First creation timestamp for that reason. |
Primary key:
(reason, imageId)This means one image can accumulate multiple state flags over time.
| Aspect | SQLite | MySQL / MariaDB | PostgreSQL |
|---|---|---|---|
| Image binary type | BLOB |
LONGBLOB |
BYTEA |
| Integer dimensions | INT |
INT UNSIGNED |
INT |
| Created timestamp | INT(11) or equivalent |
INT(11) |
INT |
| Profile FK | Not used | Not used | Not used |
image_disabled.imageId -> image.imageId |
Local FK | Local FK | Local FK |
image| Index | Column | Purpose |
|---|---|---|
idx_image_profileId |
profileId |
List images for one profile. |
idx_image_profile_album |
profileId, albumCode |
List images by profile and album. |
idx_image_created |
created |
Order recent uploads efficiently. |
image_disabled| Index | Column | Purpose |
|---|---|---|
idx_image_disabled_imageId |
imageId |
Resolve state for one image efficiently. |
idx_image_disabled_modified |
modified |
Support state-based reviews and future cleanup tasks. |
albumCode is application data, not a separate relational entity.
Current rules in application code:
-gallery and profileThe helper currently treats album codes as part of the application contract, not user-arbitrary raw text.
The subsystem also enforces runtime limits such as:
Any query or endpoint that exposes images to end users must exclude images that appear in image_disabled, regardless of reason.
That includes reasons such as:
deletedmoderatedspamPractical rule:
SELECT operations must treat any row in image_disabled as not publicly visibleThis is why the public lookup queries in the model use NOT EXISTS (...) against image_disabled.
Public delivery now applies one more rule on top of that:
In current code that means public delivery must also fail when either of these is true:
profile_disableduser_disabledSo the effective public rule is:
image_disabledprofile_disableduser_disabledThe username-based profile image route and the image-variant route both follow that stricter public visibility rule.
Admin views are different:
image_disableddeleted from the active moderation listThis is an operational choice, not a public-visibility rule.
The image exists in image and has no row in image_disabled.
The image still exists in image, but a row exists in image_disabled with reason deleted.
Expected behavior:
deleted reasonThis stage is the intended trash/paper-bin behavior.
The image row is eventually removed from image by a separate cleanup task.
This cleanup task is not implemented yet, but it should be documented as the intended design:
image_disableddeletedBecause image_disabled.imageId has a local FK to image.imageId with cascade, removing the image row will also remove matching rows in image_disabled.
Planned, not implemented yet.
Suggested behavior:
imageId rows from image_disabled where reason is deletedcreated or modified against a retention thresholdimageimage_disabled rowsThis gives the system:
| Operation | Purpose |
|---|---|
setup-base |
Creates image, image_disabled, and related indexes. |
| Operation | Purpose |
|---|---|
get-by-imageid |
Full row lookup, excluding disabled images. |
get-thumb-by-imageid |
Returns thumbImg, excluding disabled images. |
get-medium-by-imageid |
Returns mediumImg, excluding disabled images. |
get-full-by-imageid |
Returns fullImg, excluding disabled images. |
get-meta-by-imageid |
Metadata only, excluding disabled images. |
list-by-profileid |
Visible images for one profile, ordered by created DESC. |
list-by-profileid-albumcode |
Visible images for one profile and album. |
list-distinct-albumcodes-by-profileid |
Distinct visible albums for one profile. |
count-by-profileid-albumcode |
Count visible images in one album. |
| Operation | Purpose |
|---|---|
list-by-profileid-all |
Lists all profile images, including disabled ones. |
admin-get-image-disabled-by-imageid |
Returns all disabled rows for one image. |
admin-get-image-disabled-by-profileid |
Returns disabled rows for a profile’s images. |
admin-list-images-created |
Admin list ordered by upload date. |
admin-list-images-disabled-created-date |
Admin list ordered by disabled created date. |
admin-list-images-disabled-modified-date |
Admin list ordered by disabled modified date. |
| Operation | Purpose |
|---|---|
insert |
Inserts a new immutable image record. |
update-album |
Moves an image to another album code. |
upsert-image-disabled |
Adds or updates one disabled reason row. |
delete-image-disabled |
Removes one disabled reason row. |
delete |
Physically deletes one image row. |
delete-by-profileid |
Physically deletes all image rows for one profile. |
The Python helper in src/core/image.py adds higher-level rules on top of raw queries.
Important helpers include:
upload_images()
validates upload MIME, file sizes, album limits, and generates variantslist_by_profile()
returns visible image metadata and public variant URLs when configuredlist_album_codes()
returns known album codes plus defaultsget_meta()
returns visible metadata onlyget_variant()
returns a visible stored variantget_public_variant()
returns a publicly visible stored variant only when the owner profile is publicly visibledelete_image()
logical delete using image_disableddelete_by_profile()
logical delete of all images belonging to a profileadmin_list_images()
admin-oriented image listing with disabled-state inspectionset_image_disabled()
sets moderated/spam/deleted statedelete_image_disabled()
removes one disabled reasonrestore_image()
restores logically deleted images by clearing deleted reasonsinvalidate_public_username_cache()
invalidates one cached public profile-image response for /p/<username>invalidate_all_public_profile_images_cache()
invalidates all cached public image responses for one profile, including /p/<username> and /v/<image_id>/<variant>The delivery component publishes two practical route families:
<manifest.route>/p/<username><manifest.route>/v/<image_id>/<variant>Other components should not hardcode those routes. They should use schema data:
current.site.image_linkcurrent.site.image_link_profilecurrent.site.image_link_variantCurrent public delivery behavior is:
<manifest.route>/p/<username> resolves only when that username belongs to a public profile<manifest.route>/v/<image_id>/<variant> resolves only when that image is visible and its owner profile is publicPublic image delivery uses route-level response caching.
Because image ids are immutable but profile visibility and usernames can change, public cache invalidation is handled in application logic.
Current invalidation helpers are:
invalidate_public_username_cache()invalidate_all_public_profile_images_cache()Current expected usage:
/p/<old-username> and /p/<new-username>This keeps cache behavior aligned with the public visibility rules described above.
This document does not replace the delivery component documentation, but the storage model and those public routes are part of the same subsystem and should stay aligned.
image_disabledimage_disabled to image