Files
reactbin/specs/014-r2-cdn-serving/contracts/image-response.md
agatha aaacfae653 Feat: Serve images directly from Cloudflare R2 CDN
API responses now include file_url and thumbnail_url fields. When
S3_PUBLIC_BASE_URL is configured, these point to the CDN domain;
when unset, they fall back to the existing API proxy paths so local
dev requires no additional setup. UI updated to use response URL
fields directly instead of constructing proxy URLs client-side.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 00:17:22 +00:00

55 lines
2.7 KiB
Markdown

# Contract: Image Metadata Response
**Version**: 2.0 (adds `file_url`, `thumbnail_url`)
**Endpoints affected**: `GET /api/v1/images`, `GET /api/v1/images/{id}`, `POST /api/v1/images`, `PATCH /api/v1/images/{id}/tags`
## Response Schema
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"filename": "reaction.gif",
"mime_type": "image/gif",
"size_bytes": 204800,
"width": 480,
"height": 270,
"storage_key": "e3b0c44298fc1c149afbf4c8996fb924",
"thumbnail_key": "e3b0c44298fc1c149afbf4c8996fb924.thumb",
"file_url": "https://cdn.reactbin.juggalol.com/e3b0c44298fc1c149afbf4c8996fb924",
"thumbnail_url": "https://cdn.reactbin.juggalol.com/e3b0c44298fc1c149afbf4c8996fb924.thumb",
"created_at": "2026-05-08T12:00:00.000000",
"tags": ["funny", "reaction"]
}
```
## Field Descriptions
| Field | Type | Nullable | Notes |
|-------|------|----------|-------|
| `id` | string (UUID) | No | Stable image identifier |
| `hash` | string (hex) | No | SHA-256 of file content; deduplication key |
| `filename` | string | No | Original upload filename |
| `mime_type` | string | No | One of: `image/jpeg`, `image/png`, `image/gif`, `image/webp` |
| `size_bytes` | integer | No | File size in bytes |
| `width` | integer | No | Image width in pixels |
| `height` | integer | No | Image height in pixels |
| `storage_key` | string | No | Object storage key (retained for backward compat) |
| `thumbnail_key` | string | Yes | Thumbnail object storage key; null if generation failed |
| `file_url` | string | No | Full URL to fetch the image file — CDN URL in production, API proxy path in local dev |
| `thumbnail_url` | string | Yes | Full URL to fetch the thumbnail — CDN URL in production, API proxy path in local dev; null if no thumbnail |
| `created_at` | string (ISO 8601) | No | Upload timestamp |
| `tags` | string[] | No | Lowercase normalised tag list |
| `duplicate` | boolean | Yes | Present only on upload responses; true if hash matched an existing image |
## URL Behaviour
| Configuration | `file_url` example | `thumbnail_url` example |
|---------------|--------------------|------------------------|
| `S3_PUBLIC_BASE_URL` set | `https://cdn.reactbin.juggalol.com/{storage_key}` | `https://cdn.reactbin.juggalol.com/{thumbnail_key}` |
| `S3_PUBLIC_BASE_URL` not set | `/api/v1/images/{id}/file` | `/api/v1/images/{id}/thumbnail` |
## UI Contract
The UI MUST use `file_url` and `thumbnail_url` from the response to render images. The UI MUST NOT construct image URLs from `id`, `storage_key`, or `thumbnail_key` directly. The UI MUST treat `thumbnail_url: null` as "no thumbnail available" and fall back to `file_url` for display.