# 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.
