# Data Model: Upload Thumbnails **Branch**: `003-upload-thumbnails` | **Date**: 2026-05-03 ## Schema change: `images` table One nullable column is added to the existing `images` table. | Column | Type | Nullable | Default | Notes | |--------|------|----------|---------|-------| | `thumbnail_key` | `VARCHAR(70)` | YES | `NULL` | S3 object key for the WebP thumbnail. `NULL` = no thumbnail available (generation failed or pre-dates this feature). Derived value: `{image.hash}-thumb`. | No other tables change. No new tables are added. ### Migration **File**: `api/alembic/versions/002_add_thumbnail_key.py` ``` upgrade: ALTER TABLE images ADD COLUMN thumbnail_key VARCHAR(70); downgrade: ALTER TABLE images DROP COLUMN thumbnail_key; ``` --- ## ORM model change: `Image` `api/app/models.py` — `Image` class gains one field: ``` thumbnail_key: Mapped[str | None] = mapped_column(String(70), nullable=True, default=None) ``` --- ## New module: `api/app/thumbnail.py` Contains the thumbnail generation logic. Not a model, but documented here because it defines the thumbnail's shape: | Aspect | Value | |--------|-------| | Output format | WebP | | Max dimension (longest side) | 400 px | | Aspect ratio | Preserved (never upscaled) | | Source formats supported | JPEG, PNG, GIF (frame 0), WebP | | Key signature | `async def generate_thumbnail(data: bytes, mime_type: str) -> bytes` | --- ## API response shape change `_image_to_dict()` in `api/app/routers/images.py` adds `"thumbnail_key"` to its output so the UI can determine whether a thumbnail is available: ```json { "id": "...", "hash": "...", "thumbnail_key": "abc123...-thumb", ← new (null if no thumbnail) ... } ``` The UI uses the presence of `thumbnail_key` to decide whether to call `/api/v1/images/{id}/thumbnail` (with thumbnail) or fall back to `/api/v1/images/{id}/file` (without). In practice the endpoint itself handles the fallback, so the UI can always call `/thumbnail`. --- ## Storage objects per image (after this feature) | Object | Key | Format | Created at | |--------|-----|--------|-----------| | Original | `{sha256_hash}` | Original mime_type | Upload | | Thumbnail | `{sha256_hash}-thumb` | `image/webp` | Upload (same request) | Thumbnail object is deleted alongside original on image deletion.