Skip to content

Модель данных

Последняя сверка с кодом: 2026-06-12
Полная схема: backend/prisma/schema.prisma
Аудитория: аналитики, новые разработчики


1. ER-диаграмма

mermaid
erDiagram
  User ||--o{ Event : "owner"
  User ||--o{ EventRole : "has role in"
  User ||--o{ Media : "uploaded"
  User ||--o{ MediaLike : "liked"
  User ||--o{ Session : "session"

  Event ||--|| EventSettings : "settings 1:1"
  Event ||--o{ EventRole : "roles"
  Event ||--o{ Media : "media"

  Media ||--o{ MediaLike : "likes"

2. Сущности

User

Создаётся при регистрации (Better Auth). Один пользователь может быть OWNER нескольких событий.

ПолеТипСмысл
idStringUUID (Better Auth)
emailString (unique)Логин
nameStringОтображаемое имя
deletedAtDateTime?Soft-delete аккаунта (24.7)
deletedEmailString?Оригинальный email до анонимизации; grace-period для повторной регистрации
globalRoleUSER / SUPER_ADMINПлатформенная роль
planFREE / BASE / PROТариф
avatarKeyString?Ключ в storage (backlog)

Event

Событие = альбом. Создаётся только залогиненным.

ПолеТипСмысл
idStringHuman-readable slug: demo-event, svadba-x7k2
titleStringНазвание
dateDateTime?Дата события (опц.)
ownerIdString FK→UserСоздатель
deactivatedAtDateTime?Скрыто после soft-delete владельца (24.7)

Связи: media[], roles[], settings (1:1).


EventSettings

Привязан к Event 1:1 (id = eventId). Создаётся автоматически при создании Event.

ГруппаПоляСмысл
ДоступqrAccess, albumVisibility, zipAccessКто видит что (AccessScope)
UploaduploadEnabled, allowedMedia, maxFilesPerGuest, maxFileSizeMb, maxVideoDurationSecОграничения загрузки
МодерацияmoderationEnabledPENDING до approve
БрендингcoverImageKey, backgroundImageKey, brandColorStorage keys
MEMBERmemberInviteToken, allowMemberFoldersИнвайт-ссылка
ZIPzipDailyLimit(резерв)

AccessScope: ALL · MEMBERS_ONLY · OWNER_ONLY · INVITE_ONLY


EventRole

Роль конкретного User в конкретном Event. Уникально по (userId, eventId).

ПолеСмысл
roleOWNER / MODERATOR / MEMBER

OWNER всегда создаётся при POST /events. MODERATOR — OWNER назначает. MEMBER — через invite link.


Media

Одно медиа (фото или видео).

ПолеСмысл
idnanoid(16), внутренний
kindphoto или video
originalKeyStorage key оригинала
thumbKeyStorage key превью
displayKeyStorage key display (перекодированный)
posterKeyПостер видео (ffmpeg)
mime, sizeBytes, width, height, durationMsМетаданные
blurhashPlaceholder при загрузке
uploaderNameИмя гостя из localStorage
uploaderUserIdFK→User (null если гость)
contentSha256Дедупликация: уникально по (eventId, sha256)
moderationStatusPENDING / APPROVED / REJECTED
processedПревью уже создано

MediaLike

Лайк — уникальная пара (userId, mediaId). Только залогиненный.


Session / Account / Verification

Таблицы Better Auth. Не используются прямо в domain-коде — только через auth.getSession(req).


3. Что где хранится

ДанныеХранилище
Метаданные (user, event, media, roles)Postgres
Файлы (оригинал, thumb, branding)Local disk (dev) / Cloudflare R2 (prod)
Сессии SSEIn-memory (один процесс)
Имя гостяLocalStorage браузера

4. Каскадное удаление

Если удалитьУдалится также
UserSessions, EventRoles, события владельца (+ их Media/Settings)
EventEventSettings, EventRoles, Media
MediaMediaLikes

Media.uploaderUserId при удалении User → SET NULL (медиа гостей в чужих событиях сохраняется).

Удаление аккаунта (24.7)

  • Тип: soft-deletedeletedAt + анонимизация email на user.
  • При удалении: все сессии завершаются, login блокируется, события владельца → deactivatedAt, медиа → uploaderUserId SET NULL.
  • Повторная регистрация с тем же email — после grace-period (ACCOUNT_DELETE_GRACE_DAYS, default 30).

5. Индексы

  • Event: по createdAt, ownerId
  • Media: по (eventId, createdAt), (eventId, processed), (eventId, moderationStatus), uploaderUserId
  • MediaLike: по mediaId
  • EventRole: по (eventId, role)

6. Event ID — почему human-readable

svadba-anna-ivan-x7k2 — slug из title (транслит) + 4 символа nanoid.
Зачем: URL читается в чате, при печати QR-карточки, в адресной строке.
Безопасность: энтропия суффикса (~16M вариантов) + длина slug делает перебор нецелесообразным.
Media ID — обычный nanoid(16), в URL не светится.