Identity Domain (identity_v1)
The identity_v1 domain manages all aspects of user and organization identity in the CO2 target asset management system. It handles user registration, profile management, authentication integration, organization creation, estate collaboration invitations, and site-level access control with role-based permissions.
Overview
Section titled “Overview”The Identity domain is the central hub for identity and access management across the entire system. It maintains user identity state, public profiles, organizational information, invitation workflows, and fine-grained access controls for sites, layers, and features.
Key Responsibilities
Section titled “Key Responsibilities”- User Identity Management: Registration, profile updates, and account lifecycle
- Organization Identity: Creation and management of organizational entities
- Public Profiles: Public-facing user profiles with email indexing for fast lookups
- Estate Invitations: Workflow for inviting users to collaborate on estates
- Site Access Control: Role-based access management at the site level
- Layer & Feature Permissions: Fine-grained permissions for map layers and features
- Notifications: User notification inbox for system events
Package Location
Section titled “Package Location”dart_packages/co2/domains/identity_v1/├── lib/│ ├── identity_v1.dart # Main export file│ └── src/│ ├── aggregates/ # Domain aggregates│ │ ├── user_aggregate.dart│ │ ├── public_user_aggregate.dart│ │ ├── site_user_aggregate.dart│ │ ├── organization_aggregate.dart│ │ ├── user_invitation_aggregate.dart│ │ ├── notification_inbox_aggregate.dart│ │ └── public_user_email_index_aggregate.dart│ ├── events/ # Domain events│ │ ├── identity_events.dart│ │ ├── public_user_events.dart│ │ ├── site_user_events.dart│ │ ├── notification_events.dart│ │ └── public_user_email_index_events.dart│ ├── directives/ # Command handlers│ │ ├── identity_directives.dart│ │ ├── public_user_directives.dart│ │ ├── site_user_directives.dart│ │ ├── notification_directives.dart│ │ └── public_user_email_index_directives.dart│ ├── projectors/ # Read model projections│ │ └── public_user_email_index_projector.dart│ ├── selectors/ # Data query utilities│ │ └── email_lookup_selectors.dart│ └── utils/ # Utility functions│ └── email_normalization.dart├── pubspec.yaml└── test/Core Architecture
Section titled “Core Architecture”Domain Registration
Section titled “Domain Registration”The Identity domain must be registered before use. This registers all aggregates, events, directives, and read models:
import 'package:identity_v1/identity_v1.dart';
void main() { // Register all identity domain types registerIdentityV1();
// Or use the domain module wrapper final identityModule = IdentityV1(); identityModule.registerDomainTypes();}Key Aggregates
Section titled “Key Aggregates”UserAggregate
Section titled “UserAggregate”Manages user identity, profile information, and account lifecycle. This is the primary user aggregate used for authentication and profile management.
import 'package:identity_v1/identity_v1.dart';import 'package:contracts_v1/contracts_v1.dart';
// Access a user aggregate from the repositoryfinal user = userRepository.getById(userId);
// Check user status and propertiesif (user.isActive) { print('User can access the system');}
print('Display name: ${user.displayName}');print('Email: ${user.email.value}');print('Registered: ${user.registeredAt}');
// Check profile completenessif (user.hasCompleteProfile) { print('User profile is ready for use');}
// Check activityif (user.isRecentlyActive) { print('User was active within the last 30 days');}Fields:
userId(UserId): Unique user identifieremail(EmailAddress): User’s email addressstatus(UserStatus): User account status (active, deactivated, inactive)registeredAt(DateTime): Registration timestampupdatedAt(DateTime): Last profile update timestampprofile(UserProfile): Typed user profile containing name, picture, and custom fields
Business Logic Methods:
isActive,isDeactivated,isInactive: Status checksdisplayName: Full name or email fallbackhasCompleteProfile: Profile validationisRecentlyActive: Activity check (within 30 days)canBeDeactivated(),canBeReactivated(): Lifecycle transitionshasProfilePicture: Profile picture existence check
Validation Rules:
- User ID cannot be unresolved
- Email cannot be unresolved for active users
- First and last name cannot be empty for active users
- Email format must be valid (basic validation)
- Status must be a valid UserStatus value
PublicUserAggregate
Section titled “PublicUserAggregate”Manages public-facing user profiles with email uniqueness guarantees. Used for public visibility and user discovery.
import 'package:identity_v1/identity_v1.dart';
// Get a public user profilefinal publicUser = publicUserRepository.getById(userId);
// Public user propertiesprint('Display name: ${publicUser.displayName}');print('Email: ${publicUser.normalizedEmail}');print('Photo: ${publicUser.hasPhoto}');
// Check profile completenessif (publicUser.hasCompleteProfile) { print('Public profile is fully populated');}
// Activity trackingif (publicUser.isRecentlyActive) { print('User was active recently');}Fields:
userId(UserId): Unique user identifieremail(EmailAddress): User’s email (enforced lowercase for uniqueness)createdAt(DateTime): Profile creation timestampupdatedAt(DateTime): Last update timestampprofile(UserProfile): Typed user profile containing name, picture, and custom fields
Business Logic Methods:
normalizedEmail: Lowercase, trimmed email for consistent lookupshasCompleteProfile: Validation checkhasPhoto: Profile picture checkisRecentlyActive: Activity tracking (within 30 days)
Validation Rules:
- User ID and email cannot be unresolved
- Profile must have a name (firstName or lastName)
- Email format must be valid
- Email must be lowercase (business rule for consistency)
SiteUserAggregate
Section titled “SiteUserAggregate”Manages user access to specific sites with role-based permissions and fine-grained layer/feature permissions.
import 'package:identity_v1/identity_v1.dart';import 'package:contracts_v1/contracts_v1.dart';
// Get site access recordfinal siteUser = siteUserRepository.getById(aggregateId);
// Check accessif (siteUser.isActive) { // User has active access to this site
// Check layer access if (siteUser.hasLayerAccess(layerId)) { print('User can access layer'); }
// Check feature access with minimum role if (siteUser.hasFeatureAccess(featureId, Role.featureWrite)) { print('User can write to feature'); }}
// Get effective rolesfinal layerRole = siteUser.getEffectiveLayerRole(layerId);final featureRole = siteUser.getEffectiveFeatureRole(featureId);
// List all permissionsfor (final perm in siteUser.layerPermissions) { print('Layer ${perm.layerId}: ${perm.role}');}
for (final perm in siteUser.featurePermissions) { print('Feature ${perm.featureId}: ${perm.role}');}Fields:
siteId(SiteId): Site identifieruserId(UserId): User identifiersiteRole(Role): User’s site-level role (siteAdmin, siteWrite, siteRead)layerPermissions(List<LayerPermission>): Per-layer permission recordsfeaturePermissions(List<FeaturePermission>): Per-feature permission recordsgrantedAt(DateTime): When access was grantedgrantedBy(UserId): User who granted accesslastAccessedAt(DateTime?): Last access timestampisActive(bool): Whether access is currently active
Business Logic Methods:
hasLayerAccess(LayerId, [Role?]): Check layer access with optional minimum rolehasFeatureAccess(ResourceId, [Role?]): Check feature accessgetEffectiveLayerRole(LayerId): Get role considering site admin statusgetEffectiveFeatureRole(ResourceId): Get role considering site admin statuscanGrantLayerPermission(LayerId, Role): Validate permission grant eligibilitycanGrantFeaturePermission(ResourceId, Role): Validate permission grant eligibility
Permission Hierarchy:
- Site admins have implicit access to all layers and features
- Layer/feature permissions only apply when user is not a site admin
- Inactive users cannot have explicit layer or feature permissions
UserInvitationAggregate
Section titled “UserInvitationAggregate”Manages the lifecycle of estate collaboration invitations with role assignment and metadata.
import 'package:identity_v1/identity_v1.dart';import 'package:contracts_v1/contracts_v1.dart';
// Get invitationfinal invitation = invitationRepository.getById(invitationId);
// Check invitation statusif (invitation.canAccept) { print('Invitation is pending and can be accepted');}
if (invitation.isExpired) { print('Invitation has expired');}
// Get invitation detailsprint('Estate: ${invitation.estateId}');print('Role: ${invitation.role}');print('From: ${invitation.fullName}'); // title + departmentprint('Message: ${invitation.message.value}');
// Check permissionsif (invitation.hasSpecialPermissions) { print('This invitation grants special write permissions');}Fields:
invitationId(InvitationId): Unique invitation identifierestateId(EstateId): Target estate for collaborationinviteeUserId(UserId): User being invitedinvitedBy(UserId): User who sent the invitationrole(Role): Role to be assigned on acceptancedepartment(DomainText): Inviter’s departmenttitle(DomainText): Inviter’s title/positionmessage(DomainText): Custom invitation messagestatus(InvitationStatus): Invitation status (pending, accepted, declined, expired)createdAt(DateTime): Invitation creation timeupdatedAt(DateTime): Last status updaterespondedAt(DateTime?): When user respondedmetadata(InvitationMetadata): Typed metadata including expirypermissions(AccessPermissions): Invitation-specific access grants
Status Lifecycle:
pending: Initial state, awaiting user responseaccepted: User accepted, should trigger access grantdeclined: User rejected, invitation closedexpired: Invitation window closed without response
Business Logic Methods:
canAccept: Validation for acceptancecanDecline: Validation for declineisExpired: Check expiry based on metadataisResponded: Check if invitation has terminal statusfullName: Convenience getter (title + department)hasSpecialPermissions: Check for write-level permissions
Validation Rules:
- IDs cannot be unresolved
- Role cannot be empty
- Title cannot be empty
- Status must be valid
- Responded invitations must have respondedAt timestamp
- Pending invitations should not have respondedAt
OrganizationAggregate
Section titled “OrganizationAggregate”Manages organizational entity information and metadata.
import 'package:identity_v1/identity_v1.dart';import 'package:contracts_v1/contracts_v1.dart';
final org = organizationRepository.getById(organizationId);
print('Name: ${org.name.value}');print('Type: ${org.type.value}');print('Status: ${org.status.value}');print('Contact: ${org.primaryContactEmail?.value}');print('Address: ${org.organizationAddress.street}');Fields:
organizationId(OrganizationId): Unique organization identifiername(OrganizationName): Organization nametype(OrganizationType): Type of organization (corporation, nonprofit, etc.)status(OrganizationStatus): Current status (active, inactive, etc.)description(OrganizationDescription?): Optional descriptionwebsite(DomainText?): Organization website URLprimaryContactEmail(EmailAddress?): Primary contact emailprimaryContactPhone(DomainText?): Primary contact phoneregistrationNumber(DomainText?): Business registration numbertaxId(DomainText?): Tax ID or VAT numberorganizationAddress(OrganizationAddress): Typed address objecttypedOrganizationData(OrganizationData): Extended organization metadatacreatedBy(UserId): User who created the organizationcreatedAt(DateTime): Creation timestampupdatedAt(DateTime): Last update timestamp
NotificationInboxAggregate
Section titled “NotificationInboxAggregate”Manages user notifications and their read/unread status.
import 'package:identity_v1/identity_v1.dart';
final inbox = notificationRepository.getById(userId);
// Get notifications (sorted by creation date, newest first)for (final notification in inbox.notifications) { print('${notification.title}: ${notification.message}'); if (notification.readAt != null) { print('Read at: ${notification.readAt}'); }}
// Find unread countfinal unreadCount = inbox.notifications .where((n) => n.readAt == null) .length;print('Unread: $unreadCount');Fields:
userId(UserId): Owner of the inboxnotifications(List<UserNotificationEntry>): List of notifications (sorted newest first)createdAt(DateTime): Inbox creation timeupdatedAt(DateTime): Last modification time
Validation:
- User ID cannot be unresolved or empty
PublicUserEmailIndexAggregate
Section titled “PublicUserEmailIndexAggregate”Maintains a denormalized index of email addresses to user IDs for efficient lookup of public users by email.
import 'package:identity_v1/identity_v1.dart';
// This aggregate is used internally for email lookups// typically through the email_lookup_selectorsfinal userIds = emailLookupSelectors.findUsersByEmail('user@example.com');Domain Events
Section titled “Domain Events”Events represent important changes in the Identity domain. These are immutable records of what happened.
User Identity Events
Section titled “User Identity Events”UserRegisteredEvent
Section titled “UserRegisteredEvent”Fired when a new user account is created.
final event = UserRegisteredEvent( userId: UserId('user-123'), email: EmailAddress.fromString('user@example.com'), firstName: FirstName.fromString('John'), lastName: LastName.fromString('Doe'), registeredAt: DateTime.now(),);Payload:
userId: New user’s IDemail: Registration email addressfirstName: User’s first namelastName: User’s last nameregisteredAt: Registration timestamp
UserProfileUpdatedEvent
Section titled “UserProfileUpdatedEvent”Fired when user profile information is updated.
final event = UserProfileUpdatedEvent( userId: userId, updatedProfile: UserProfile( firstName: FirstName.fromString('Jane'), profilePictureUrl: FileUrl.fromString('https://example.com/pic.jpg'), ), updatedAt: DateTime.now(),);Payload:
userId: User being updatedupdatedProfile: Typed profile changes (only changed fields needed)updatedAt: Update timestamp
UserDeactivatedEvent
Section titled “UserDeactivatedEvent”Fired when a user account is deactivated.
final event = UserDeactivatedEvent( userId: userId, reason: DomainText('Account requested deletion'), deactivatedBy: adminUserId, deactivatedAt: DateTime.now(),);Payload:
userId: User being deactivatedreason: Optional deactivation reasondeactivatedBy: Admin who performed the actiondeactivatedAt: Deactivation timestamp
Organization Events
Section titled “Organization Events”OrganizationCreatedEvent
Section titled “OrganizationCreatedEvent”Fired when a new organization is created.
final event = OrganizationCreatedEvent( organizationId: OrganizationId('org-123'), name: OrganizationName.fromString('Acme Corp'), type: OrganizationType.corporation, description: OrganizationDescription.fromString('A company'), website: DomainText('https://acme.example.com'), primaryContactEmail: EmailAddress.fromString('contact@acme.example.com'), organizationAddress: OrganizationAddress( street: 'Main Street', city: 'Springfield', postalCode: '12345', ), createdBy: userId, createdAt: DateTime.now(), typedOrganizationData: const OrganizationData(),);OrganizationUpdatedEvent
Section titled “OrganizationUpdatedEvent”Fired when organization information changes.
OrganizationStatusChangedEvent
Section titled “OrganizationStatusChangedEvent”Fired when organization status changes (active, inactive, etc.).
Public User Events
Section titled “Public User Events”PublicUserCreatedEvent
Section titled “PublicUserCreatedEvent”Fired when a public user profile is created.
final event = PublicUserCreatedEvent( userId: userId, email: email, displayName: DomainText('John Doe'), photoUrl: FileUrl.fromString('https://example.com/photo.jpg'), createdAt: DateTime.now(), userProfile: UserProfile( firstName: FirstName.fromString('John'), lastName: LastName.fromString('Doe'), ),);PublicUserProfileUpdatedEvent
Section titled “PublicUserProfileUpdatedEvent”Fired when public profile information changes.
Estate Invitation Events
Section titled “Estate Invitation Events”UserInvitedToEstateEvent
Section titled “UserInvitedToEstateEvent”Fired when a user is invited to collaborate on an estate.
final event = UserInvitedToEstateEvent( invitationId: invitationId, estateId: estateId, inviteeUserId: inviteeUserId, invitedBy: currentUserId, role: Role.estateWrite, department: DomainText('Operations'), title: DomainText('Estate Manager'), message: DomainText('Please join our estate'), createdAt: DateTime.now(), typedInvitationMetadata: const InvitationMetadata(), typedAccessPermissions: const AccessPermissions(),);UserInvitationAcceptedEvent
Section titled “UserInvitationAcceptedEvent”Fired when a user accepts an invitation.
final event = UserInvitationAcceptedEvent( invitationId: invitationId, inviteeUserId: userId, acceptedAt: DateTime.now(),);UserInvitationDeclinedEvent
Section titled “UserInvitationDeclinedEvent”Fired when a user declines an invitation.
final event = UserInvitationDeclinedEvent( invitationId: invitationId, inviteeUserId: userId, declinedAt: DateTime.now(), reason: DomainText('Not interested'),);Site Access & Permission Events
Section titled “Site Access & Permission Events”SiteUserAccessGrantedEvent
Section titled “SiteUserAccessGrantedEvent”Fired when a user is granted access to a site.
final event = SiteUserAccessGrantedEvent( siteId: siteId, userId: userId, role: Role.siteWrite, grantedBy: adminUserId, grantedAt: DateTime.now(),);SiteUserRoleChangedEvent
Section titled “SiteUserRoleChangedEvent”Fired when a user’s site role is changed.
SiteUserAccessRevokedEvent
Section titled “SiteUserAccessRevokedEvent”Fired when a user’s site access is revoked.
LayerPermissionGrantedEvent
Section titled “LayerPermissionGrantedEvent”Fired when a user is granted permission to access a specific layer.
final event = LayerPermissionGrantedEvent( siteId: siteId, userId: userId, layerId: layerId, role: Role.layerWrite, grantedBy: adminUserId, grantedAt: DateTime.now(),);LayerPermissionRevokedEvent
Section titled “LayerPermissionRevokedEvent”Fired when layer permission is revoked.
FeaturePermissionGrantedEvent
Section titled “FeaturePermissionGrantedEvent”Fired when a user is granted permission to use a feature.
FeaturePermissionRevokedEvent
Section titled “FeaturePermissionRevokedEvent”Fired when feature permission is revoked.
Notification Events
Section titled “Notification Events”NotificationAddedToInboxEvent
Section titled “NotificationAddedToInboxEvent”Fired when a notification is added to a user’s inbox.
NotificationMarkedReadInInboxEvent
Section titled “NotificationMarkedReadInInboxEvent”Fired when a user marks a notification as read.
Domain Directives
Section titled “Domain Directives”Directives are commands that drive the domain, causing aggregates to apply events.
User Registration
Section titled “User Registration”import 'package:identity_v1/identity_v1.dart';import 'package:contracts_v1/contracts_v1.dart';
// Create a registration directivefinal directive = RegisterUserDirective( payload: RegisterUserPayload( userId: UserId('user-123'), email: EmailAddress.fromString('user@example.com'), firstName: FirstName.fromString('John'), lastName: LastName.fromString('Doe'), ),);
// Execute the directive (framework handles execution)// This will create a UserRegisteredEvent and update the UserAggregateProfile Management
Section titled “Profile Management”// Update user profilefinal directive = UpdateUserProfileDirective( payload: UpdateUserProfilePayload( userId: userId, updatedProfile: UserProfile( firstName: FirstName.fromString('Jane'), profilePictureUrl: FileUrl.fromString('https://example.com/pic.jpg'), ), ),);
// Deactivate user accountfinal deactivateDirective = DeactivateUserDirective( payload: DeactivateUserPayload( userId: userId, reason: DomainText('Account deletion requested'), deactivatedBy: adminUserId, ),);Organization Management
Section titled “Organization Management”final directive = CreateOrganizationDirective( payload: CreateOrganizationPayload( organizationId: OrganizationId('org-123'), name: OrganizationName.fromString('Acme Corp'), type: OrganizationType.corporation, // ... other fields ),);Estate Invitations
Section titled “Estate Invitations”// Accept an invitationfinal acceptDirective = AcceptEstateInvitationDirective( payload: AcceptEstateInvitationPayload( invitationId: invitationId, acceptedBy: userId, ),);
// Decline an invitationfinal declineDirective = DeclineEstateInvitationDirective( payload: DeclineEstateInvitationPayload( invitationId: invitationId, declinedBy: userId, reason: DomainText('Not interested'), ),);Site Access Control
Section titled “Site Access Control”// Grant site accessfinal grantDirective = GrantSiteAccessDirective( id: DirectiveId('directive-1'), payload: JsonValue({'siteId': siteId.value, 'userId': userId.value, ...}),);
// Change site user rolefinal changeRoleDirective = ChangeSiteUserRoleDirective( id: DirectiveId('directive-2'), payload: JsonValue({'siteId': siteId.value, 'userId': userId.value, 'newRole': ...}),);
// Revoke site accessfinal revokeDirective = RevokeSiteAccessDirective( id: DirectiveId('directive-3'), payload: JsonValue({'siteId': siteId.value, 'userId': userId.value}),);
// Grant layer permissionfinal grantLayerDirective = GrantLayerPermissionDirective( id: DirectiveId('directive-4'), payload: JsonValue({'siteId': siteId.value, 'userId': userId.value, 'layerId': layerId.value, ...}),);
// Grant feature permissionfinal grantFeatureDirective = GrantFeaturePermissionDirective( id: DirectiveId('directive-5'), payload: JsonValue({'siteId': siteId.value, 'userId': userId.value, 'featureId': featureId.value, ...}),);Identity and Permissions Relationship
Section titled “Identity and Permissions Relationship”The Identity domain provides the foundation for the Permissions domain, which enforces fine-grained access control throughout the system.
How Identity Feeds Permissions
Section titled “How Identity Feeds Permissions”- User Registration: UserRegisteredEvent creates the initial user identity
- Site Access: SiteUserAccessGrantedEvent establishes site-level membership
- Role Assignment: SiteUserRoleChangedEvent updates permission level
- Layer Permissions: LayerPermissionGrantedEvent restricts access to specific map layers
- Feature Permissions: FeaturePermissionGrantedEvent controls feature-specific access
Permission Hierarchy
Section titled “Permission Hierarchy”System Admin ↓Site Admin (implies all layer and feature access) ├─ Layer Admin (for specific layers) │ └─ Layer Write │ └─ Layer Read │ ├─ Feature Admin │ └─ Feature Write │ └─ Feature Read │ └─ [Estate/Site level roles]Access Control Examples
Section titled “Access Control Examples”// Check if user can view a specific layerbool canViewLayer(SiteUserAggregate access, LayerId layerId) { return access.hasLayerAccess(layerId, Role.layerRead);}
// Check if user can edit a featurebool canEditFeature(SiteUserAggregate access, ResourceId featureId) { return access.hasFeatureAccess(featureId, Role.featureWrite);}
// Site admins get implicit access to everythingbool isSiteAdmin(SiteUserAggregate access) { return access.siteRole.hasMinimumPermission(Role.siteAdmin);}Email Management
Section titled “Email Management”Email Normalization
Section titled “Email Normalization”The domain provides email normalization utilities for consistent handling:
import 'package:identity_v1/identity_v1.dart';
// Normalize email for comparisonfinal normalized = EmailNormalization.normalize('User@Example.COM');// Result: 'user@example.com'
// Check email equivalenceif (EmailNormalization.areEquivalent(email1, email2)) { print('Emails are the same');}
// Validate email formatif (EmailNormalization.isValidEmail('test@example.com')) { print('Email is valid');}Email Lookup
Section titled “Email Lookup”The domain maintains a denormalized email index for fast public user lookups:
import 'package:identity_v1/identity_v1.dart';
// Find public users by email (returns UserId list)final userIds = emailLookupSelectors.findUsersByEmail('user@example.com');Public User Profiles
Section titled “Public User Profiles”Public user profiles are separate from internal user accounts and serve the public-facing aspects of the system.
import 'package:identity_v1/identity_v1.dart';
// Create a public user profilefinal directive = CreatePublicUserDirective( payload: CreatePublicUserPayload( userId: userId, email: email, displayName: DomainText('John Doe'), photoUrl: FileUrl.fromString('https://example.com/photo.jpg'), ),);
// Update public profilefinal updateDirective = UpdatePublicUserProfileDirective( payload: UpdatePublicUserProfilePayload( userId: userId, updatedProfile: UserProfile( firstName: FirstName.fromString('Jane'), profilePictureUrl: FileUrl.fromString('https://example.com/new-photo.jpg'), ), ),);Notifications
Section titled “Notifications”The domain provides user notification inbox management:
import 'package:identity_v1/identity_v1.dart';
// Create a notificationfinal directive = CreateNotificationDirective( payload: CreateNotificationPayload( userId: userId, title: DomainText('New collaboration invite'), message: DomainText('You have been invited to Estate ABC'), relatedResourceId: estateId.value, relatedResourceType: 'Estate', ),);
// Mark notification as readfinal readDirective = MarkNotificationReadDirective( payload: MarkNotificationReadPayload( userId: userId, notificationId: NotificationId('notif-123'), ),);Common Patterns
Section titled “Common Patterns”User Registration Flow
Section titled “User Registration Flow”// 1. Register new user (creates UserAggregate)final registerDirective = RegisterUserDirective( payload: RegisterUserPayload( userId: UserId('user-123'), email: EmailAddress.fromString('user@example.com'), firstName: FirstName.fromString('John'), lastName: LastName.fromString('Doe'), ),);
// 2. Create public profile (creates PublicUserAggregate)final publicDirective = CreatePublicUserDirective( payload: CreatePublicUserPayload( userId: userId, email: email, displayName: DomainText('John Doe'), ),);
// 3. Add email to index (creates PublicUserEmailIndexAggregate entry)final indexDirective = AddPublicUserEmailIndexEntryDirective( payload: AddPublicUserEmailIndexEntryPayload( email: email, userId: userId, ),);Estate Collaboration Invitation Flow
Section titled “Estate Collaboration Invitation Flow”// 1. Invite user to estate (creates UserInvitationAggregate)final inviteEvent = UserInvitedToEstateEvent( invitationId: invitationId, estateId: estateId, inviteeUserId: inviteeUserId, invitedBy: currentUserId, role: Role.estateWrite, department: DomainText('Operations'), title: DomainText('Estate Manager'), message: DomainText('Please join our estate'), createdAt: DateTime.now(),);
// 2a. User accepts (creates SiteUserAccessGrantedEvent)final acceptDirective = AcceptEstateInvitationDirective( payload: AcceptEstateInvitationPayload( invitationId: invitationId, acceptedBy: userId, ),);
// 2b. System grants site access (triggered by invitation acceptance)final grantAccessEvent = SiteUserAccessGrantedEvent( siteId: siteId, userId: userId, role: Role.estateWrite, grantedBy: UserId.system, grantedAt: DateTime.now(),);Access Modification Flow
Section titled “Access Modification Flow”// 1. Grant layer permissionfinal grantDirective = GrantLayerPermissionDirective( id: DirectiveId('directive-1'), payload: JsonValue({ 'siteId': siteId.value, 'userId': userId.value, 'layerId': layerId.value, 'role': {'type': 'layer_write'}, }),);
// 2. Later, change the permissionfinal changeDirective = ChangeSiteUserRoleDirective( id: DirectiveId('directive-2'), payload: JsonValue({ 'siteId': siteId.value, 'userId': userId.value, 'newRole': {'type': 'site_admin'}, }),);
// 3. Eventually, revoke accessfinal revokeDirective = RevokeSiteAccessDirective( id: DirectiveId('directive-3'), payload: JsonValue({ 'siteId': siteId.value, 'userId': userId.value, }),);Integration Points
Section titled “Integration Points”With Contracts Domain
Section titled “With Contracts Domain”The Identity domain uses strongly-typed value objects from contracts_v1:
UserId,EstateId,SiteId: Entity identifiersEmailAddress,FirstName,LastName: Named value objectsRole: Permission levels across the systemUserProfile,OrganizationAddress: Structured data- Various enumeration types:
UserStatus,OrganizationStatus, etc.
With Other Domains
Section titled “With Other Domains”- Permissions Domain: Enforces fine-grained access control based on Identity aggregates
- Policy Domain: Manages access policies and security rules
- Estate/Site Domains: User access is defined in Identity domain, estate structures manage physical hierarchy
- Intent Domain: User-initiated commands flow through directives registered in Identity domain
Deprecation Notes
Section titled “Deprecation Notes”Some fields in aggregates have been deprecated in favor of more strongly-typed alternatives:
// Deprecated: Use profile.firstName instead@Deprecated('Use profile.firstName instead')String get firstName => profile.firstName?.value ?? '';
// Deprecated: Use profile instead for type-safe access@Deprecated('Use profile instead for type-safe access')Map<String, dynamic> get profileData => profile.toJson();Always use the typed profile field and its properties (firstName, lastName, profilePictureUrl) instead of accessing the untyped profileData map.
Testing
Section titled “Testing”The Identity domain includes comprehensive test coverage:
test/├── user_aggregate_test.dart├── public_user_aggregate_test.dart├── site_user_aggregate_test.dart├── user_invitation_aggregate_test.dart├── notification_inbox_aggregate_test.dart├── identity_directives_test.dart├── public_user_directives_test.dart├── site_user_directives_test.dart├── notification_directives_test.dart├── identity_v1_test.dart└── ...Test examples:
test('UserAggregate can be registered and created', () { registerIdentityV1();
final event = UserRegisteredEvent( userId: UserId('user-123'), email: EmailAddress.fromString('user@example.com'), firstName: FirstName.fromString('John'), lastName: LastName.fromString('Doe'), registeredAt: DateTime.now(), );
final user = UserAggregate.empty().apply(event);
expect(user.isActive, true); expect(user.email.value, 'user@example.com'); expect(user.displayName, 'John Doe');});Best Practices
Section titled “Best Practices”- Always use typed value objects: Never pass raw strings for IDs or structured data
- Validate on aggregate creation: Use aggregate
validate()method to enforce business rules - Normalize emails: Use
EmailNormalizationfor consistent email handling - Check status before operations: Verify aggregate status before attempting state transitions
- Use unresolved values: For optional IDs, use the
unresolvedconstants rather than null - Profile management: Always update the strongly-typed
profilefield, not deprecatedprofileData - Permission checks: Use
hasLayerAccess()andhasFeatureAccess()with proper role checks - Invitation lifecycle: Respect invitation status transitions (pending → accepted/declined)
Dependencies
Section titled “Dependencies”dependencies: nomos_core: ^1.0.0 nomos_persistence_memory: ^1.0.0 contracts_v1: ^1.0.0 intents_v1: ^1.0.0 policy_v1: ^1.0.0Version
Section titled “Version”- Package: identity_v1
- Version: 1.0.0
- Dart SDK:
>=3.3.0 <4.0.0