Перейти к содержимому

Схема базы данных (Prisma)

🗂️ Интерактивная схема — ER-диаграммы всех доменов, поля каждой таблицы, политика удаления на связях и поиск по таблицам. Страница собирается из schema.prisma при каждой сборке документации, поэтому не расходится с кодом. Локально: npm run dev:docs, затем /schema/.

Единая PostgreSQL-схема проекта описана в файле apps/core-api/prisma/schema.prisma. Это единственный источник истины для структуры БД: из него генерируется Prisma-клиент (@prisma/client), который переиспользуется всеми сервисами монорепозитория через @crewsforge-back/apis/utils/prisma-client (libs/apis/utils/prisma-client).

Технические особенности схемы:

  • СУБД — PostgreSQL (datasource db), строка подключения берётся из env("POSTGRES_URL").
  • Генератор клиента — prisma-client-js, binaryTargets = ["native", "linux-musl-openssl-3.0.x"] (второй таргет нужен для запуска в Alpine/musl-контейнерах).
  • Все первичные ключи — UUID со значением по умолчанию gen_random_uuid() на стороне БД (@default(dbgenerated("gen_random_uuid()")) @db.Uuid).
  • Имена таблиц и колонок в БД задаются через @@map / @map в snake_case, модели и поля в коде — в camelCase.
  • Аудиторские поля created_at / updated_at есть у большинства таблиц. У append-only журналов (state_transitions, outbox_events, artifacts, milestone_submissions, specifications, dispute_evidences, concierge_messages, notifications, timers, webhook_events, walrider_thread_states) и у части связок updated_at нет намеренно: строка не меняется после записи.
  • Деньги хранятся только в минорных единицах (BigInt, поля *Minor), валюта — на проекте (Project.currency), у денежных записей не дублируется. Decimal в схеме не используется.
  • Доли и ставки — в basis points (Int, поля *Bps): commissionRateBps, teamShareBps, completedCriteriaBps.
  • Всего в схеме 61 модель и 42 enum-типа.

Схема покрывает весь домен платформы — от заявки заказчика до расчёта по этапу и разбора спора — и является целевой: часть таблиц ещё не имеет пишущего кода, но их форма зафиксирована baseline’ом и раскатана на всех окружениях.

#ДоменМоделиКто пишет
1Пользователи, роли, профили19auth-api, user-api
2Токены и сессии2auth-api
3Гео-справочник5сид GeoSeeder
4Проект1project-api, admin-api
5Walrider (AI-агент)3ai-agent-service, project-api
6Команда3user-api
7Спецификация и план2project-api
8Этапы, критерии, сдача, разбор6project-api
9Контрактинг4project-api
10Деньги4сервис биллинга (единственный пишущий)
11Спор и расторжение4project-api, admin-api
12Операционный контур5платформенный воркер; читают рабочие места admin-api
13Консьерж2admin-api
14Интеграция трекера1интеграционный слой

Колонка «кто пишет» отражает проектное разграничение из docs/crewsforge-architecture-session-2026-08-21/; сервисы биллинга и воркера в репозитории пока не заведены.

Правило одно: сделочная цепочка не удаляется каскадом. Строки проекта, этапов, контрактов, холдов, расчётов, споров и расторжений — доказательство того, что произошло между сторонами, и удаление вышестоящей записи не имеет права их стереть.

УчастокПравилоПочему
User → Customer, User → EmployeeRestrictпод пользователем лежит сделочная история
Customer → Project, Team → ProjectRestrictпроект — корень цепочки
Project → Specification / PlanVersion / Milestone / Party / Contract / Offer / TerminationRestrictдокументы сделки
Milestone → Artifact / Submission / Review / Hold / Dispute / CriterionRestrictсдача и приёмка
Hold → Settlement, Party → PartyDocument, Dispute → Position → EvidenceRestrictденьги и доказательства
Team → TeamMember, Employee → TeamMemberRestrictподписи и сдачи ссылаются на членство; ушедший участник получает status = REMOVED
Профильные сателлиты (Profile, Avatar, связки навыков, языков, портфолио, токены, сессии)Cascadeпроизводные данные, доказательной ценности не несут
Project → WalriderThreadCascadeтред консультации — не часть сделки
Profile → Timezone / LocationSetNullсправочник может измениться, профиль остаётся

Всего в схеме 27 связей с onDelete: Restrict.

Жёсткого удаления пользователя не существует. Запрос на удаление аккаунта — это User.deletedAt плюс анонимизация персональных данных; сделочные записи остаются. Мягкое удаление есть и у проекта (Project.deletedAt), но только в статусе DRAFT и только на уровне сервиса.

Правило проверяется интеграционным тестом apps/core-api-e2e/src/schema/deal-chain-foreign-keys.integration.spec.ts: он читает information_schema и падает, если на любом внешнем ключе сделочной таблицы появилось CASCADE или незнакомый ключ.

Prisma Schema Language не умеет ни CHECK-констрейнтов, ни частичных уникальных индексов, поэтому три правила живут в baseline-миграции как raw SQL. Держать их в приложении нельзя: их нарушение обязано отвергаться базой даже при гонке двух транзакций или при прямом доступе к БД.

ИнвариантФормаЧто запрещает
settlements_amounts_sum_to_holdCHECKрасчёт по холду, в котором team + founder + fee ≠ hold_amount_minor. Денормализованное hold_amount_minor лежит в самой строке settlements ровно ради этой проверки: CHECK не умеет ходить в соседнюю таблицу
queue_items_open_dedupчастичный UNIQUE по (type, subject_type, subject_id) WHERE status IN ('OPEN','IN_PROGRESS')две незакрытые задачи очереди по одному предмету. Повторная задача того же типа заводится свободно после того, как предыдущая ушла в DONE/CANCELLED
users_on_roles_single_platform_roleчастичный UNIQUE по (user_id) WHERE role_id IN (<три uuid>)совмещение двух платформенных ролей одним человеком. Клиентские роли (CUSTOMER, EMPLOYEE) в предикат не входят — их сочетать можно
roles_type_keyUNIQUE по roles.typeвторую строку справочника ролей того же типа. Без неё предыдущий индекс обходится: он перечисляет id платформенных ролей, и роль ADMIN с другим uuid его предикат не видит

Граница названа прямо, чтобы на неё не рассчитывали как на защиту:

  • CHECK сверяет расчёт с денормализованным hold_amount_minor, а не с самим холдом. Строка, где hold_amount_minor не равен holds.amount_minor, базой принимается. Сверка — транзакционный инвариант сервиса, вводится вместе с расчётом выплаты (M4).
  • Число расчётов по одному холду ничем не ограничено. Три полные выплаты одного холда база примет. Единственность неотменённого расчёта — тоже правило M4/M5: SPLIT по модели шага 2 §7 это одна строка с двумя ненулевыми суммами, а не две строки.
  • executed_by_user_id, assignee_user_id, closed_by_user_id — uuid без внешнего ключа. Так объявлено шагом 2 (полиморфные и actor-ссылки живут без FK); ссылка на несуществующего человека базой принимается. Для Settlement, который по A-1/D-5 является доказательством «кто исполнил выплату», это осознанная слабость — усиливать её решает M4.

Предикат третьего индекса содержит литеральные uuid, а не подзапрос к roles: предикат частичного индекса обязан быть IMMUTABLE. Те же значения продублированы в libs/apis/shared/src/lib/constants/roles.constants.ts и в сиде ролей. Менять их можно только вместе с пересозданием индекса миграцией.

Тексты и обоснования — в хвосте apps/core-api/prisma/migrations/20260830000000_baseline/migration.sql; поведение закреплено интеграционными тестами apps/core-api-e2e/src/schema/*.integration.spec.ts.


erDiagram
User ||--o| Customer : "customer"
User ||--o| Employee : "employee"
User ||--o| Profile : "profile"
User ||--o{ UserOnRole : "userOnRole"
Role ||--o{ UserOnRole : "userOnRole"
Profile ||--o| Avatar : "avatar"
Profile ||--o| CustomerProfile : "customerProfile"
Profile ||--o| EmployeeProfile : "employeeProfile"
Profile ||--o{ ProfileLanguageLink : "languages"
ProfileLanguage ||--o{ ProfileLanguageLink : "profiles"
Customer ||--o| CustomerProfile : "customerProfile"
Employee ||--o| EmployeeProfile : "employeeProfile"
Employee ||--o{ TeamMember : "teamMembers"
EmployeeProfile ||--o{ EmployeeProfileSkill : "skills"
EmployeeProfile ||--o{ EmployeeProfileSpecialization : "specializations"
EmployeeProfile ||--o{ Portfolio : "portfolios"
Skill ||--o{ EmployeeProfileSkill : "employeeProfilesSkills"
Skill ||--o{ PortfolioSkill : "portfoliosSkills"
Specialization ||--o{ EmployeeProfileSpecialization : "employeeProfilesSpecializations"
Specialization ||--o{ Portfolio : "portfolios"
Portfolio ||--o{ PortfolioSkill : "skills"
Portfolio ||--o{ PortfolioLink : "links"
Portfolio ||--o{ PortfolioMedia : "media"
Модель (таблица)НазначениеКлючевые поляОграничения и индексы
User (users)учётная записьemail, phone, passwordHash/passwordSalt (nullable — у платформенных пользователей пароля нет), isEmailVerified, isBlocked, deletedAt@@unique на email и phone, @@index([isBlocked])
Role (roles)справочник ролейtype: RoleTypeстроки — данные baseline’а с детерминированными id (см. seeds)
UserOnRole (users_on_roles)связка пользователь ↔ рольuserId, roleId@@unique([userId, roleId]) + частичный индекс одной платформенной роли
Customer (customers)сторона-заказчик (фаундер)userId@@unique(userId), userRestrict
Employee (employees)сторона-исполнительuserId, bio@@unique(userId), userRestrict
Profile (profiles)персональные данныеfirstName, lastName, timezoneId, locationId@@unique(userId); справочники — SetNull
Avatar (avatars)аватар профиляurl, format@@unique(profileId)
CustomerProfile (customers_profiles)связь профиля с заказчикомcustomerId, profileIdоба @unique
EmployeeProfile (employee_profiles)профессиональная часть профиляexperienceMonths, workType: WorkTypeemployeeId/profileId @unique
EmployeeProfileSkill (employees_profiles_skills)навыки исполнителя@@unique([employeeProfileId, skillId]), без полей аудита
EmployeeProfileSpecialization (employees_profiles_specializations)специализации исполнителя@@unique([employeeProfileId, specializationId]), без полей аудита
Skill (skills)справочник навыковname, descriptionname @unique
Specialization (specializations)справочник специализацийname, descriptionname @unique
ProfileLanguage (profile_languages)справочник языковcode, namecode @unique
ProfileLanguageLink (profiles_languages)язык профиля с уровнемlevel@@unique([profileId, languageId]), без полей аудита
Portfolio (portfolios)кейс исполнителяtitle, description, specializationId
PortfolioSkill (portfolios_skills)навыки кейса@@unique([portfolioId, skillId])
PortfolioLink (portfolios_links)ссылки кейсаtype: PortfolioLinkType, url
PortfolioMedia (portfolios_media)медиа кейсаurl, format
Модель (таблица)НазначениеКлючевые поляОграничения
Token (tokens)пара access/refreshaccessTokenId, accessTokenExpiredAt (unix, Int), refreshTokenId, refreshTokenExpiredAtuserCascade, session 1:1
Session (sessions)сессия устройстваtokenId, fingerprint, ip (@map("ip_address"))tokenId @unique, @@unique([userId, fingerprint])

Наполняется сидом GeoSeeder (см. seeds), кодом платформы не пишется.

Модель (таблица)НазначениеКлючевые поляОграничения и индексы
Timezone (timezones)таймзона вида UTC+01:00namename @unique
Country (countries)страна из RestCountriesISO-коды cca2/cca3/ccn3/cioc, nameCommon/nameOfficial, region/subregion, координаты, population: BigInt, JSON-блоки (languages, currencies, flags, maps, translations, …)четыре ISO-кода @unique; индексы по region, subregion, nameCommon
Location (locations)дерево локацийtype: LocationType, name, parentId (самоссылка LocationTree), countryId, lat/lng, meta@@unique([type, name, parentId]); индексы по [type, name], countryId, parentId
CountryTimezone (countries_timezones)связка страна ↔ таймзона@@unique([countryId, timezoneId]), @@index([timezoneId])
LocationTimezone (locations_timezones)связка локация ↔ таймзона@@unique([locationId, timezoneId]), @@index([timezoneId])

Project — корень сделочной цепочки. Все денежные и организационные атрибуты сделки висят на нём.

ПолеТипОписание
idString @db.UuidPK
title, descriptionStringзаголовок и описание
categoryProjectCategoryType?категория, необязательна
statusProjectStatus @default(DRAFT)статус, 10 значений
contractingStageContractingStage?подстадия; заполнена только в статусе CONTRACTING
customerIdString @db.Uuidвладелец (Customer), Restrict
teamIdString? @db.Uuidназначается при мэтчинге (Team), Restrict
currencyString @default("USD") @db.Char(3)валюта сделки — одна на проект
budgetMinMinor, budgetMaxMinorBigInt?вилка бюджета AI-этапа, в минорных единицах
belowBudgetGateBoolean @default(false)мягкий гейт: проект ниже порога требует ручного одобрения на мэтчинг
commissionRateBpsInt?ставка комиссии в basis points, снапшот на контрактинге
createdAt, updatedAtDateTimeаудит
deletedAtDateTime?мягкое удаление, допустимо только в DRAFT

Индексы: customerId, teamId, status, category, deletedAt. Связи 1:N: specifications, planVersions, milestones, parties, contracts, offers, terminations (все Restrict); 1:1 — walriderThread (Cascade).

Бюджет проекта — только вилка: точная сумма появляется в PlanVersion.totalAmountMinor и в суммах этапов. Единого поля «цена проекта» в схеме нет намеренно.

Смены статуса через HTTP сейчас нет ни в project-api, ни в admin-api: прежняя матрица переходов снята вместе с маршрутами (см. project-api).

Модель (таблица)НазначениеКлючевые поляОграничения и индексы
WalriderThread (walrider_threads)тред диалога, 1:1 с проектомcustomerId, projectId, externalThreadIdprojectId @unique, @@index([customerId]), projectCascade
WalriderMessage (walrider_messages)сообщение тредаmessageId (autoincrement), role: WalriderMessageRole, content, externalMessageId, metadata@@index([threadId])
WalriderThreadState (walrider_thread_states)снимок состояния треда/сообщенияstatus, dataиндексы по threadId, messageId; только createdAt
erDiagram
Team ||--o{ TeamMember : "members"
Team ||--o{ TeamInvitation : "invitations"
Team ||--o| PayoutAccount : "payoutAccount"
Team ||--o{ Project : "projects"
Team ||--o{ Offer : "offers"
Employee ||--o{ TeamMember : "teamMembers"
Модель (таблица)НазначениеКлючевые поляОграничения и индексы
Team (teams)команда-исполнительname, slug, bio, status: TeamStatus, externalExperience (кейсы до появления истории на платформе), verifiedAtslug @unique, @@index([status])
TeamMember (team_members)членство сотрудника в командеrole: TeamMemberRole, canSign, canSubmit, status: TeamMemberStatus, removedAt@@unique([teamId, employeeId]), @@index([employeeId]), обе связи Restrict
TeamInvitation (team_invitations)приглашение в команду по e-mailemail, role, token, status: InvitationStatus, expiresAttoken @unique, @@index([teamId]), teamCascade

Две оси членства независимы. role — уровень доступа к рабочему пространству (ADMIN/EDITOR/VIEWER), canSign/canSubmit — полномочия подписать контракт и сдать этап. Администратор команды может не иметь права сдачи, наблюдатель — иметь. Ушедший участник не удаляется, а переводится в status = REMOVED: на членство ссылаются подписи и сдачи.

Модель (таблица)НазначениеКлючевые поляОграничения
Specification (specifications)версия спецификации проектаversion, content (структурированная спека; бюджет — только вилка {minMinor, maxMinor}), authorType: AuthorType, authorUserId, status: SpecificationStatus, sourceThreadId@@unique([projectId, version]), только createdAt
PlanVersion (plan_versions)версия плана этаповversion, items (JSON: orderIndex, title, description, amountMinor, durationDays, criteria[]), totalAmountMinor, status: PlanVersionStatus, internalNotes, authorMemberId, sentAt, respondedAt, rejectReason@@unique([projectId, version])

PlanVersion.internalNotes — торг платформы с командой. Это поле никогда не попадает в DTO заказчика; авторство фиксируется полем authorType спецификации, чтобы AI-черновик не выдавался за человеческий.

erDiagram
Project ||--o{ Milestone : "milestones"
Milestone ||--o{ AcceptanceCriterion : "criteria"
Milestone ||--o{ Artifact : "artifacts"
Milestone ||--o{ MilestoneSubmission : "submissions"
Milestone ||--o{ AcceptanceReview : "reviews"
Milestone ||--o| Hold : "hold"
Milestone ||--o{ Dispute : "disputes"
AcceptanceReview ||--o{ ReviewRejectionItem : "items"
Модель (таблица)НазначениеКлючевые поляОграничения и индексы
Milestone (milestones)оплачиваемый этап работplanVersionId (null = этап создан из разбора), orderIndex, title, description, amountMinor, status: MilestoneStatus, dueAt, deadlinePausedAt@@unique([projectId, orderIndex]), @@index([status])
AcceptanceCriterion (acceptance_criteria)проверяемый критерий приёмкиorderIndex, text, state: CriterionState@@unique([milestoneId, orderIndex])
Artifact (artifacts)результат работыtype: ArtifactType, title, url, fileKey, uploaderMemberId, source: ArtifactSource@@index([milestoneId]), ни deletedAt, ни каскада: артефакты оплаченных этапов остаются у заказчика навсегда
MilestoneSubmission (milestone_submissions)попытка сдачи этапаattempt, submittedByMemberId (требует canSubmit), coverNote@@unique([milestoneId, attempt]); лимита пересдач нет
AcceptanceReview (acceptance_reviews)разбор непринятияsubmissionId, openedByUserId, verdict: ReviewVerdict, reasoning, decidedByUserId, newMilestoneId, decidedAtобоснование обязательно при вердикте
ReviewRejectionItem (review_rejection_items)пункт отказаcriterionId, comment@@unique([reviewId, criterionId])

Отказ принять этап обязательно привязан к критерию (ReviewRejectionItem.criterionId); свободный комментарий — только дополнение. Разбор и спор останавливают срок этапа: на входе проставляется deadlinePausedAt, на выходе dueAt сдвигается на длительность паузы.

Модель (таблица)НазначениеКлючевые поляОграничения
Party (parties)снапшот юридических данных стороны на проектside: DealSide, legalName, legalForm, countryCode, registrationNumber, taxId, address (JSON), kycState: KycState@@unique([projectId, side])
PartyDocument (party_documents)документ стороныkind, fileKey, status: PartyDocumentStatus, reviewedByUserId
Contract (contracts)договор со сторонойside (TEAM = договор A, FOUNDER = договор B), planVersionId (приложение к рамочному), status: ContractStatus, templateVersion, documentHash (sha256 отправленного PDF), envelopeExternalId, signerUserId/signerMemberId, signedAt, signedFileKey, terminatedAt@@unique([projectId, side])
Offer (offers)предложение команде и заказчикуteamId, state: OfferState, createdByUserId, declineReason, respondedAtиндексы по projectId, teamId

Юрданные — снапшот на проект, а не общая переиспользуемая карточка: реквизиты на момент подписания обязаны остаться неизменными. Договоров всегда два, и они расторгаются независимо (Contract.terminatedAt на каждой стороне). Порядок оффера — сначала команда, затем заказчик — зашит в OfferState.

Единственный пишущий сервис этого домена — биллинг. Остальные читают.

Модель (таблица)НазначениеКлючевые поляОграничения
PayoutAccount (payout_accounts)выплатной аккаунт командыprovider (stripe), externalAccountId, state: PayoutAccountState, requirements (JSON currently_due провайдера)teamId @unique
Hold (holds)заморозка суммы этапаamountMinor, state: HoldState, paymentIntentExternalId, chargeExternalId, heldAt, closedAtmilestoneId @unique
Settlement (settlements)распределение холдаteamAmountMinor, founderAmountMinor, platformFeeMinor, holdAmountMinor (денормализация ради CHECK), basisType: SettlementBasis, basisId, executedByUserId, status: SettlementStatus, transferExternalId, refundExternalId, executedAtCHECK settlements_amounts_sum_to_hold
WebhookEvent (webhook_events)входящее событие провайдераprovider: WebhookProvider, externalEventId, payload, status: WebhookEventStatus, processedAt@@unique([provider, externalEventId]) — идемпотентность: повторное событие не порождает повторного движения денег

Settlement.basisType + basisId — обязательное основание расчёта: разбор, решение арбитра, оценка при расторжении или двойное одобрение. Расчёта без основания в схеме быть не может.

Модель (таблица)НазначениеКлючевые поляОграничения
Dispute (disputes)спор по этапуstate: DisputeState, initiatorSide, openedByUserId, positionDeadlineAt, outcome: DisputeOutcome, teamShareBps (при PARTIAL), reasoning, arbiterUserId, resolvedAt, lciaEscalatedAtспор открывается только по этапу в FUNDED и позже
DisputePosition (dispute_positions)позиция стороныside, statement, submittedByUserId, submittedAt@@unique([disputeId, side]); statement = null при истёкшем таймере — зафиксированный факт неответа
DisputeEvidence (dispute_evidences)доказательство к позицииtitle, fileKey, urlтолько createdAt
Termination (terminations)расторжение проектаstate: TerminationState, initiatorSide, requestedByUserId, basis: TerminationBasis, objectionDeadlineAt, objectionByUserId, objectionDisputeId, completedCriteriaBps (мера выполненного объёма), assessedByUserId, reasoning, sourceDisputeId, executedAt

lciaEscalatedAt — не расторжение, а долгая пауза: эскалация во внешний арбитраж останавливает внутренний процесс, не закрывая проект. Возражение против расторжения превращается в спор (objectionDisputeId).

Пять таблиц без единого внешнего ключа: предмет задаётся парой subjectType + subjectId (полиморфная ссылка). Это осознанный выбор — одна очередь и один журнал обслуживают все домены.

Модель (таблица)НазначениеКлючевые поляОграничения и индексы
StateTransition (state_transitions)append-only журнал переходовsubjectType: SubjectType, subjectId, fromState, toState, actorType: ActorType, actorUserId, actorRole, basisType, basisId, reason, metadata@@index([subjectType, subjectId, createdAt]); нет updatedAt и удаления
OutboxEvent (outbox_events)исходящие события шиныeventType (= routing key, например milestone.status.changed), aggregateType, aggregateId, payload, publishedAt, attempts@@index([publishedAt, createdAt]) под запрос relay
QueueItem (queue_items)задача рабочего местаtype: QueueItemType, targetRole: PlatformRole, subjectType/subjectId, projectId (денормализация для скоупинга), status: QueueItemStatus, dueAt, overdueMarkedAt, assigneeUserId, closedByUserId, resultTransitionId@@index([targetRole, status, createdAt]), @@index([subjectType, subjectId]) + частичный индекс дедупа
Timer (timers)отложенное срабатываниеkind: TimerKind, subjectType/subjectId, firesAt, firedAt, cancelledAt@@index([firesAt, firedAt, cancelledAt])
Notification (notifications)уведомление пользователюtype (= eventType источника), subjectType/subjectId, requiresAction, payload, readAt@@index([userId, readAt, createdAt])

В QueueItem нет денежных полей — оператор видит задачу, но не сумму. Дефолтная сортировка очереди — возраст (createdAt), поэтому индекс включает его третьим ключом. Журнал переходов пишет только библиотека машины состояний.

Модель (таблица)НазначениеКлючевые поляОграничения и индексы
ConciergeThread (concierge_threads)обращение в поддержкуopenedByUserId, side: DealSide? (null = вне контекста сделки), projectId?, subject, status: ConciergeStatus@@index([status, createdAt])
ConciergeMessage (concierge_messages)сообщение обращенияauthorUserId, authorRole (FOUNDER/TEAM/OPERATOR/ADMIN), bodythreadRestrict, только createdAt
Модель (таблица)НазначениеКлючевые поляОграничения
TrackerLink (tracker_links)связь сущности платформы с объектом внешнего трекераsubjectType (PROJECT/MILESTONE/TEAM), subjectId, externalType (workspace/board/member), externalId@@unique([subjectType, subjectId, externalType])

EnumЗначенияГде используется
RoleTypeCUSTOMER, EMPLOYEE, ADMIN, OPERATOR, ARBITERRole.type
WorkTypeFULL_TIME, PART_TIME, CONTRACT, FREELANCEEmployeeProfile.workType
PortfolioLinkTypeGITHUB, DEMO, PROD, OTHERPortfolioLink.type
LocationTypeREGION, SUBREGION, COUNTRY, CITYLocation.type
ProjectStatusDRAFT, AI_CONSULTATION, SPEC_READY, TEAM_MATCHING, TEAM_PROPOSED, CONTRACTING, ACTIVE, COMPLETED, DISPUTED, CANCELLEDProject.status
ContractingStagePLAN_DRAFTING, PLAN_INTERNAL_REVIEW, KYC_PENDING, PLAN_SENT, SIGNINGProject.contractingStage
ProjectCategoryTypeWEB_DEVELOPMENT, MOBILE_DEVELOPMENT, DESIGN, MARKETING, COPYWRITING, DATA_SCIENCE, DEV_OPS, QA_TESTING, OTHERProject.category
WalriderMessageRoleUSER, ASSISTANTWalriderMessage.role
TeamStatusONBOARDING, ACTIVE, SUSPENDED, ARCHIVEDTeam.status
TeamMemberRoleADMIN, EDITOR, VIEWERTeamMember.role, TeamInvitation.role
TeamMemberStatusACTIVE, REMOVEDTeamMember.status
InvitationStatusPENDING, ACCEPTED, DECLINED, EXPIRED, REVOKEDTeamInvitation.status
AuthorTypeAI, HUMANSpecification.authorType
SpecificationStatusDRAFT, PROPOSED, ACCEPTED, SUPERSEDEDSpecification.status
PlanVersionStatusDRAFT, INTERNAL_REVIEW, SENT, ACCEPTED, REJECTED, SUPERSEDEDPlanVersion.status
MilestoneStatusPLANNED, PLAN_APPROVED, FUNDED, IN_PROGRESS, SUBMITTED, FOUNDER_APPROVED, VERIFIED, PAID, REJECTED, DISPUTEDMilestone.status
CriterionStateUNCHECKED, MET, DISPUTED, CONFIRMEDAcceptanceCriterion.state
ArtifactTypeFILE, LINK, DEMOArtifact.type
ArtifactSourcePLATFORM, TRACKERArtifact.source
ReviewVerdictCRITERIA_UNMET_RETURN, CRITERIA_MET_NEW_SCOPE, ESCALATED_TO_ARBITRATIONAcceptanceReview.verdict
DealSideFOUNDER, TEAMParty.side, Contract.side, Dispute.initiatorSide, DisputePosition.side, Termination.initiatorSide, ConciergeThread.side
KycStateNOT_REQUIRED, PENDING, SUBMITTED, APPROVED, REJECTEDParty.kycState
PartyDocumentStatusUPLOADED, APPROVED, REJECTEDPartyDocument.status
ContractStatusDRAFT, SENT_FOR_SIGNING, SIGNED, TERMINATEDContract.status
OfferStateSENT_TO_TEAM, TEAM_ACCEPTED, DECLINED_BY_TEAM, ACCEPTED_BY_FOUNDER, DECLINED_BY_FOUNDER, WITHDRAWNOffer.state
PayoutAccountStateNOT_STARTED, PENDING, RESTRICTED, ACTIVEPayoutAccount.state
HoldStatePENDING_IN, HELD, RELEASING, RELEASED, REFUNDED, SPLITHold.state
SettlementBasisDUAL_APPROVAL, REVIEW_VERDICT, ARBITER_DECISION, TERMINATION_ASSESSMENTSettlement.basisType
SettlementStatusPENDING, EXECUTED, FAILEDSettlement.status
WebhookProviderSTRIPE, SIGNING, TRACKERWebhookEvent.provider
WebhookEventStatusRECEIVED, PROCESSED, SKIPPED_DUPLICATE, FAILEDWebhookEvent.status
DisputeStateOPENED, POSITIONS_GATHERING, UNDER_ARBITRATION, RESOLVED, ESCALATED_LCIADispute.state
DisputeOutcomeTEAM_FAVOR, FOUNDER_FAVOR, PARTIALDispute.outcome
TerminationStateREQUESTED, UNDER_REVIEW, EXECUTED, WITHDRAWNTermination.state
TerminationBasisTEAM_FAULT, FOUNDER_INITIATIVE, TEAM_INITIATIVE, DISPUTE_DECISIONTermination.basis
SubjectTypePROJECT, MILESTONE, HOLD, CONTRACT, DISPUTE, TERMINATION, OFFER, TEAM, PAYOUT_ACCOUNT, PLAN_VERSION, CONCIERGE_THREADStateTransition, OutboxEvent, QueueItem, Timer, Notification, TrackerLink
ActorTypeUSER, SYSTEM, TIMERStateTransition.actorType
PlatformRoleOPERATOR, ADMINISTRATOR, ARBITERQueueItem.targetRole
QueueItemStatusOPEN, IN_PROGRESS, DONE, CANCELLEDQueueItem.status
QueueItemTypePLAN_APPROVAL, SUBMISSION_VERIFICATION, CONCIERGE_FIRST_LINE, ACCEPTANCE_REVIEW, PLAN_MEDIATION, TEAM_MATCHING, PRICE_REVIEW, KYC_DOCUMENTS, TERMINATION_ASSESSMENT, TEAM_ONBOARDING, ESCALATED_INQUIRY, BUDGET_GATE_REVIEW, FOUNDER_SILENCE_DECISION, SETTLEMENT_EXECUTION, DISPUTE_ARBITRATIONQueueItem.type
TimerKindFOUNDER_SILENCE, DISPUTE_POSITION_WINDOW, TERMINATION_OBJECTION_WINDOW, ENVELOPE_EXPIRY, MILESTONE_DEADLINE, SLA_TARGET, REMINDERTimer.kind
ConciergeStatusOPEN, ESCALATED, RESOLVED, CLOSEDConciergeThread.status

Два множества имён платформенной роли. RoleType несёт ADMIN — это значение едет в claim’е platformRole admin-токена. PlatformRole несёт ADMINISTRATOR — оно типизирует QueueItem.targetRole. Расхождение намеренное; мост между множествами — карта PLATFORM_ROLE_BY_ROLE_TYPE в libs/apis/shared/src/lib/constants/roles.constants.ts, и любой код обязан ходить через неё, а не сравнивать строки.

Соответствие значений SubjectType и союзов каталога событий api-core-domain-events закреплено unit-тестом apps/core-api/prisma/schema-enums.spec.ts: расхождение хотя бы в одном значении роняет прогон nx test core-api.

Добавить поле или модель: правится schema.prisma, затем nx run core-api:prisma:migrate:dev (новая миграция поверх baseline’а) и nx run core-api:prisma:schema:generate. Обновляются соответствующий раздел этой страницы и строка в migrations.

Добавить значение в enum: значение в schema.prisma + миграция. Если enum участвует в каталоге событий (SubjectType), союз в api-core-domain-events правится в том же коммите — иначе падает schema-enums.spec.ts.

Добавить таблицу в сделочную цепочку: внешние ключи объявляются onDelete: Restrict, а имя таблицы добавляется в DEAL_CHAIN_TABLES теста внешних ключей — иначе тест не даст новому ключу проехать незамеченным.

Добавить инвариант, который Prisma не выражает: raw SQL в новой миграции (не в baseline — он уже применён на окружениях) плюс интеграционный тест рядом с существующими в apps/core-api-e2e/src/schema/.