# Архитектура ## Назначение `image-platform` - отдельный control plane для своего Cloudinary-like image pipeline. Проект отвечает за metadata, S3 artifacts, variants, allowlist, presets и генерацию изображений через внешний `imgproxy`. ## Компоненты | Компонент | Статус | Роль | |---|---|---| | PostgreSQL | сейчас | Источник правды для assets, variants, hosts, statuses | | S3/MinIO | сейчас | Хранилище originals и generated variants | | Backend | сейчас | NestJS JSON API, Swagger, PostgreSQL/S3/RabbitMQ orchestration | | Worker | позже | RabbitMQ consumer, imgproxy processing, upload в S3, update PostgreSQL | | Admin UI | сейчас | React/Vite UI для будущего управления hosts/assets/variants/presets | | Gateway | сейчас | Fastify public image origin, L1 memory cache, root routing, без DB/S3 доступа | | RabbitMQ | сейчас | Очередь задач генерации variants | | imgproxy | external | CPU-heavy image processing | ## Архитектурное решение Нужное поведение - Cloudinary-like: публичный URL изображения сам запускает read-through pipeline, если variant ещё не создан. `image-platform` строится ради совместимости с `next/image` как custom loader provider. Next.js application не должен заранее вызывать API, ждать генерацию и затем подставлять S3 URL. Он должен передавать `src`, `width` и `quality` в loader, а loader должен вернуть стабильный URL нашего image origin. Gateway поэтому является обязательной частью public delivery path, а не опциональным кешем. ## Целевая delivery схема ```text client -> CDN optional -> Fastify gateway L1 memory cache -> NestJS backend -> PostgreSQL + S3 ready variant -> RabbitMQ -> worker -> external imgproxy -> source image ``` Read-through flow: ```text 1. client запрашивает /images/{assetId}/v{version}/{preset}?w=640&q=80&f=auto 2. CDN HIT -> ответ сразу 3. Gateway L1 HIT -> ответ сразу 4. Gateway L1 MISS -> Gateway вызывает Backend internal ensure endpoint 5. Backend проверяет PostgreSQL/S3 6. S3 HIT -> Backend стримит bytes Gateway, Gateway кладёт ответ в L1 7. S3 MISS -> Backend ставит RabbitMQ job 8. Worker вызывает external imgproxy и сохраняет результат в S3 9. Worker обновляет PostgreSQL, Backend отдаёт готовые bytes Gateway 10. Gateway кладёт ответ в L1 и отдаёт клиенту ``` ## Разделение ответственности PostgreSQL отвечает на вопросы: - какие assets зарегистрированы; - какие variants созданы; - где variants лежат в S3; - какие variants `pending`, `processing`, `ready`, `failed`; - сколько bytes занимает asset/project/user; - какие source hosts разрешены. S3 хранит байты: - original images, если решим сохранять originals; - generated variants; - metadata объектов на уровне storage, но не бизнес-логику. Gateway отдаёт картинки: - L1 memory HIT - сразу из памяти; - L1 memory MISS - вызывает Backend; - не имеет доступа к PostgreSQL, S3 и RabbitMQ. Backend управляет процессами: PostgreSQL, S3 read, RabbitMQ enqueue, ожидание ready variant. Worker выполняет CPU-heavy generation через external imgproxy и пишет результат в S3. ## URL модель Публичные URL должны быть стабильными и не раскрывать source URL. Для Next/image provider основной URL должен принимать width/quality из loader: ```text /images/{assetId}/v{version}/{preset}?w={width}&q={quality}&f=auto ``` Примеры: ```text /images/asset_123/v4/card?w=640&q=80&f=auto /images/asset_123/v4/hero?w=1920&q=80&f=auto ``` `v{version}` берётся из `image_assets.current_version` и меняется при обновлении source image. Это даёт immutable cache без purge старых CDN/L1/S3 keys. `f=auto` нужен для совместимости с `next/image` custom loader: Next передаёт в loader `src`, `width` и `quality`, но не выбирает AVIF/WebP сам при custom loader. Image origin должен выбрать формат по `Accept` header, как Cloudinary `f_auto`. Из-за `f=auto` обязательно: - S3 key должен включать фактически выбранный формат; - response должен выставлять `Vary: Accept`; - CDN и Gateway L1 cache должны учитывать `Accept`; - response должен выставлять `Cache-Control: public, max-age=31536000, immutable` для versioned assets. Для ручного ``/`srcset` можно добавить явный формат позже: ```text /images/{assetId}/v{version}/{preset}?w=640&q=80&f=avif /images/{assetId}/v{version}/{preset}?w=640&q=80&f=webp /images/{assetId}/v{version}/{preset}?w=640&q=80&f=jpg ``` ## External imgproxy `imgproxy` не входит в этот проект и не деплоится вместе с ним. Он подключается через env: ```env IMGPROXY_UPSTREAM=http://external-imgproxy.internal:8080 ``` Это позволяет держать image processing на отдельной мощной машине и не рисковать основным сервером.