6. Domain Model
This document describes the domain model for the CrewsForge platform, with emphasis on the changes introduced by the Admin Service feature. All diagrams and definitions are framework-agnostic and reflect domain-level concepts rather than persistence details.
6.1 Aggregate and Entity Class Diagram
Заголовок раздела «6.1 Aggregate and Entity Class Diagram»classDiagram class User { +UUID id +String email +String? phone +String? passwordHash +String? passwordSalt +Boolean isEmailVerified +Boolean isBlocked +DateTime createdAt +DateTime updatedAt +block() void +unblock() void +isAdmin() Boolean +hasPassword() Boolean +canAuthenticate() Boolean }
class Role { +UUID id +RoleType type +DateTime createdAt +DateTime updatedAt }
class UserOnRole { +UUID id +UUID userId +UUID roleId +DateTime createdAt +DateTime updatedAt }
class Customer { +UUID id +UUID userId +DateTime createdAt +DateTime updatedAt }
class Employee { +UUID id +UUID userId +String? bio +DateTime createdAt +DateTime updatedAt }
class Profile { +UUID id +UUID userId +String? firstName +String? lastName +UUID? timezoneId +UUID? locationId +DateTime createdAt +DateTime updatedAt }
class Project { +UUID id +String title +String description +ProjectCategoryType? category +ProjectStatus status +UUID customerId +DateTime? deletedAt +DateTime createdAt +DateTime updatedAt +softDelete() void +restore() void +isDeleted() Boolean +transitionStatus(ProjectStatus newStatus) void +canTransitionTo(ProjectStatus newStatus) Boolean }
class Session { +UUID id +UUID userId +UUID tokenId +String fingerprint +String ip +DateTime createdAt +DateTime updatedAt }
class Token { +UUID id +UUID userId +UUID accessTokenId +Int accessTokenExpiredAt +String refreshTokenId +Int refreshTokenExpiredAt +DateTime createdAt +DateTime updatedAt }
class RoleType { <<enumeration>> CUSTOMER EMPLOYEE ADMIN }
class ProjectStatus { <<enumeration>> DRAFT PUBLISHED HIRING IN_PROGRESS REVIEW COMPLETED CANCELLED DISPUTED }
class ProjectCategoryType { <<enumeration>> WEB_DEVELOPMENT MOBILE_DEVELOPMENT DESIGN MARKETING COPYWRITING DATA_SCIENCE DEV_OPS QA_TESTING OTHER }
User "1" --> "0..1" Customer User "1" --> "0..1" Employee User "1" --> "0..1" Profile User "1" --> "*" UserOnRole User "1" --> "*" Token User "1" --> "*" Session UserOnRole "*" --> "1" Role Role --> RoleType Customer "1" --> "*" Project Project --> ProjectStatus Project --> ProjectCategoryType Token "1" --> "0..1" Session6.2 Value Objects
Заголовок раздела «6.2 Value Objects»| Value Object | Fields | Invariants |
|---|---|---|
| TokenPayload | userId: UUID, roles: RoleType[], customerId?: UUID, employeeId?: UUID, iat: number, exp: number, jti: UUID | Must contain at least one role. For ADMIN role, customerId and employeeId are always null. jti must be a valid UUID matching a Token record. |
| VerificationCode | code: String(6), email: String, ttl: 900s, cooldown: 120s | Exactly 6 alphanumeric characters. Stored in Redis with key pattern ADMIN_VERIFICATION_CODE:email={email}. One-time use — deleted from Redis immediately after successful verification. Cooldown key ADMIN_VERIFICATION_RESEND_CODE:email={email} prevents resending within 120 seconds. |
| AdminAllowedDomains | domains: String[] | Parsed from env variable ADMIN_ALLOWED_DOMAINS (comma-separated). Must contain at least one domain. Domain comparison is case-insensitive. Only the domain portion of the email (after @) is matched. |
| ProjectStatusTransition | from: ProjectStatus, to: ProjectStatus[] | Transitions are strictly defined by the PROJECT_STATUS_TRANSITIONS map. COMPLETED and CANCELLED are terminal states with no outgoing transitions. Any transition not in the map is rejected. |
6.3 Aggregate Boundaries
Заголовок раздела «6.3 Aggregate Boundaries»graph TB subgraph UserAggregate["User Aggregate (Root: User)"] User["User"] UserOnRole["UserOnRole"] Session["Session"] Token["Token"] Profile["Profile"] end
subgraph CustomerAggregate["Customer Aggregate (Root: Customer)"] Customer["Customer"] CustomerProfile["CustomerProfile"] end
subgraph EmployeeAggregate["Employee Aggregate (Root: Employee)"] Employee["Employee"] EmployeeProfile["EmployeeProfile"] end
subgraph ProjectAggregate["Project Aggregate (Root: Project)"] Project["Project"] WalriderThread["WalriderThread"] WalriderMessage["WalriderMessage"] WalriderThreadState["WalriderThreadState"] end
subgraph RoleAggregate["Role Aggregate (Root: Role)"] Role["Role"] end
User -.->|"references"| Customer User -.->|"references"| Employee UserOnRole -.->|"references"| Role Customer -.->|"owns"| ProjectAggregate rules:
- User Aggregate — The User entity is the aggregate root. It owns UserOnRole (role assignments), Sessions, Tokens, and Profile. Admin is not a separate entity; it is a User with an ADMIN role assignment via UserOnRole. All blocking/unblocking operations go through the User root.
- Customer Aggregate — Customer is the aggregate root. It is linked to User but managed independently. Owns CustomerProfile.
- Employee Aggregate — Employee is the aggregate root. It is linked to User but managed independently. Owns EmployeeProfile and its nested structures (skills, specializations, portfolios).
- Project Aggregate — Project is the aggregate root. Owns WalriderThread and its messages/states. Soft delete (
deletedAt) and status transitions are enforced at the aggregate root level. - Role Aggregate — Role is a reference entity. Roles are system-defined (seeded) and immutable at runtime.
6.4 Business Invariants
Заголовок раздела «6.4 Business Invariants»-
Admin cannot block another admin. Before executing a block operation, the system must verify that the target user does not hold the ADMIN role. If the target has an ADMIN role via
UserOnRole, the operation is rejected. -
Admin must register with a corporate email domain. The domain portion of the admin’s email must match one of the entries in
ADMIN_ALLOWED_DOMAINS. Registration with a non-matching domain is rejected at the service layer before any user record is created. -
Verification code is one-time use. Upon successful verification, the code is immediately deleted from Redis. A second attempt to verify the same code will fail. This applies to both registration and login flows.
-
Soft-deleted projects are excluded from all queries by default. Any query that retrieves projects must include a
deletedAt IS NULLfilter. This applies to both admin and non-admin endpoints. A project with a non-nulldeletedAtis considered logically deleted. -
Project status transitions must follow the defined transition map. A status change from state A to state B is permitted only if B is in the allowed targets for A as defined by
PROJECT_STATUS_TRANSITIONS. Any transition not in the map is rejected with a validation error. -
COMPLETED and CANCELLED are terminal project states. Once a project reaches COMPLETED or CANCELLED status, no further status transitions are allowed. These states have empty transition target lists.
-
Admin has no password. For users with only the ADMIN role,
passwordHashandpasswordSaltare null. Authentication is exclusively passwordless (email + verification code). The nullable password fields must not break existing login flows for Customer/Employee users. -
Blocked users cannot authenticate. When
isBlockedis true, all authentication attempts (login, token refresh) must be rejected. Existing Passport strategies for Customer and Employee must check theisBlockedflag on every request. -
A user can hold multiple roles simultaneously. The
UserOnRolejunction table supports many-to-many relationships. A single user could theoretically hold CUSTOMER and EMPLOYEE roles (or any combination), but a user with ADMIN role should not also hold CUSTOMER or EMPLOYEE roles (admin is a distinct persona). -
Verification code cooldown prevents abuse. A new verification code cannot be sent to the same email within 120 seconds of the previous send. The cooldown is enforced via a separate Redis key with TTL of 120 seconds.
-
All admins are equal — no hierarchy. There is no superadmin concept. Every user with the ADMIN role has identical permissions. Access control is binary: either a user has the ADMIN role or they do not.
-
Session uniqueness is scoped to user and fingerprint. The composite unique constraint
(userId, fingerprint)ensures one session per device per user. Creating a new session with the same fingerprint replaces the existing one.
6.5 Domain Events
Заголовок раздела «6.5 Domain Events»| Event | Trigger | Payload | Consumers |
|---|---|---|---|
| AdminRegistered | Admin completes registration (email verified, user + role created) | { userId, email, roles: ['ADMIN'], timestamp } | Audit log (future), notification service |
| UserBlocked | Admin blocks a non-admin user | { userId, blockedByAdminId, email, timestamp } | Auth service (invalidate active sessions), notification service |
| UserUnblocked | Admin unblocks a user | { userId, unblockedByAdminId, email, timestamp } | Notification service |
| ProjectSoftDeleted | Admin soft-deletes a project | { projectId, deletedByAdminId, customerId, timestamp } | Search index (remove from results), notification service |
| ProjectStatusChanged | Admin changes project status via transition | { projectId, previousStatus, newStatus, changedByAdminId, timestamp } | Notification service, analytics |
| ProjectUpdatedByAdmin | Admin edits project fields (title, description, category) | { projectId, updatedFields: string[], changedByAdminId, timestamp } | Audit log (future) |
| AdminLoginCompleted | Admin successfully logs in (code verified) | { userId, email, ip, fingerprint, timestamp } | Audit log (future), security monitoring |
Note: Domain events are listed here for architectural completeness. The initial Admin Service MVP may not publish all events via a message broker. At minimum, service-layer method calls serve as synchronous event boundaries. Asynchronous event publishing via RabbitMQ can be introduced incrementally as consumer services are built.