# Design ## Context Greenfield-сервис «многопользовательский фотоальбом». Существующая инфраструктура: PostgreSQL (`pg.localnet:5432`) и Garage S3 (`s3.localnet`, path-style, регион `garage`, ключ `GK...`). Жёсткое ограничение — без Docker: один статичный Go-бинарник со вшитой статикой SvelteKit через `go:embed`, запуск через systemd. Мотивация — в proposal.md. ## Goals / Non-Goals **Goals:** - Один артефакт (Go-бинарник), `CGO_ENABLED=0`, self-hosted поверх Garage + PostgreSQL. - Чёткая модель прав, проверяемая на уровне сервиса, а не только UI. - Live-обновления через SSE без внешних зависимостей. **Non-Goals:** - Горизонтальное масштабирование (один инстанс, in-memory SSE-хаб). - Web push (браузерные push) — отложено; уведомления только in-app через SSE. - Публичный каталог альбомов — доступ вне состава только по share-ссылке. - Zip-скачивание альбомов. ## Decisions ### 1. Обработка изображений: `imaging` + `gen2brain/webp` + `goexif` Resize — `disintegration/imaging`, WebP-кодирование — `gen2brain/webp` (libwebp, транспилированный в чистый Go/WASM; `kolesa-team/go-webp` отклонён, т.к. это cgo-биндинг, несовместимый с `CGO_ENABLED=0`), EXIF — `rwcarlsen/goexif`. Всё чистый Go, совместимо с `CGO_ENABLED=0`. - **Альтернатива (отклонена):** `bimg`/libvips — быстрее и нативный WebP/EXIF, но требует CGO и libvips на сборочной и прод-машинах, ломает «статичный бинарник». - **Альтернатива (запасная):** `imaging` + JPEG-превью — проще, но превью тяжелее ~25–35%. - Конвейер: stream original в S3 → EXIF+resize → encode WebP → thumb/preview в S3. ### 2. Аутентификация: JWT access + refresh с хранением refresh-токена Access 15 мин, refresh 30 дней. Refresh-токены хранятся в отдельной таблице `refresh_tokens` (хэшированными) — это даёт отзыв при logout и ротацию при refresh. - **Альтернатива:** stateless refresh-JWT без хранения — проще, но logout не может отозвать токен (противоречит спец `auth`). - SSE: `EventSource` не умеет заголовки, поэтому access-токен в `?token=`; короткий TTL компенсирует риск. (Cookie+CSRF — отложено.) ### 3. Права: «нет эскалации», владелец из `albums.owner_id` Роли `admin/editor/member/viewer` хранятся в `album_members`. Владелец — создатель, выводится из `albums.owner_id`, не дублируется в составе. Правило: никто не может выдать роль выше своей; админ управляет `editor/member/viewer`, владелец — админами; редактор приглашает только `member/viewer`. Проверка на уровне сервиса. ### 4. Скачивание: только одиночное фото `GET /api/photos/:id/download` — presigned URL оригинала. Zip альбома исключён из MVP. ### 5. Доступ по ссылке: таблица `share_links` вместо `privacy` `privacy` на `albums` убран. `share_links` (id, album_id, token, created_by, created_at, expires_at, revoked_at) — несколько ссылок, срок при создании, точечный отзыв. Доступ по токену — view-only (сетка, лайтбокс, одиночное скачивание), без комментариев/лайков/загрузки. Публичные эндпоинты `/api/share/:token/*` отдают свежие presigned URL. ### 6. Presigned URL: TTL 1 час + выдача по требованию Клиент не хранит URL надолго: сетка получает свежие thumb/preview при каждом запросе списка, лайтбокс дёргает `GET /api/photos/:id` перед показом оригинала. SSE-событие несёт `thumb_url` только для мгновенного рендера. Короткий TTL сохраняет малое окно утечки для приватных фото. ### 7. SSE: in-memory hub на каналах `map[albumID]map[clientID]chan Event`, неблокирующая публикация, отключение при переполнении буфера, heartbeat 25 сек, отдельная горутина на клиента. Догон пропущенного — REST (`photos?since=`). Один инстанс (см. Non-Goals); на рестарте клиенты переподключаются. ### 8. Фреймворк и структура Backend — Gin + pgx + AWS SDK v2 (path-style, регион `garage`). Frontend — SvelteKit 2 + Svelte 5, adapter-static, статика в `backend/static` через `go:embed`. Миграции — golang-migrate. Логи — slog. Конфиг — env (12-factor). ## Risks / Trade-offs - **[Чистый Go медленнее libvips]** → Приемлемо для MVP; превью генерируются асинхронно, не в пути запроса. При росте нагрузки — миграция на bimg с отказом от `CGO_ENABLED=0`. - **[In-memory SSE-хаб теряет события при рестарте]** → Клиенты переподключаются и догоняют через REST; окно потери ограничено. - **[Токен в query-параметре]** → Короткий TTL access-токена и refresh в httpOnly-хранилище на фронте; Cookie+CSRF как дальнейшее улучшение. - **[Presigned URL в 1 час]** → Может «протухать» на долгоживущих вкладках → компенсируется выдачей по требованию (лайтбокс, свежий список). - **[Несколько share-ссылок]** → Небольшой оверхед на управление, но даёт точечный отзыв (важно для сценария «фотограф ↔ несколько клиентов»). ## Migration Plan Greenfield: golang-migrate применяет `up` к `pg.localnet` при деплое. Таблицы: users, albums (без `privacy`), album_members, invites, photos, comments, likes, notifications, share_links, refresh_tokens. Откат — `down`-миграции или переустановка предыдущего бинарника.