Tenant Service — Domain Model
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · 02 DDD · 04 Events
1. Aggregates
| Aggregate | Root entity | Primary ID | Invariants |
|---|---|---|---|
| Tenant | Tenant | TenantId (ten_) | slug globally unique; status transitions constrained; activation is one-time |
| HierarchyNode | HierarchyNode | NodeId (nod_) | Exactly one root node per tenant; parent must belong to same tenant; node_type from allowed list |
| Role | Role | RoleId (rol_) | code unique within tenant; built-in roles immutable |
| RoleAssignment | RoleAssignment | RoleAssignmentId (roa_) | One role per (user, node) pair; user must be member of node |
| UserProfile | UserProfile | ProfileId (prf_) | One profile per UserId within tenant; userId references identity-service |
| OrgMembership | OrgMembership | MembershipId (mem_) | One membership per (user, node) pair; node must belong to same tenant |
| AccessPolicy | AccessPolicy | PolicyId (pol_) | name unique within tenant; ABAC attribute conditions valid JSON Schema |
2. State machines
2.1 Tenant status
2.2 HierarchyNode
2.3 OrgMembership
3. Entities (non-root)
| Entity | Parent aggregate | Purpose |
|---|---|---|
| TenantConfiguration | Tenant | KV store of tenant settings (mfa_required, branding, etc.) |
| SubscriptionRecord | Tenant | Immutable log of subscription tier/date changes |
| NodeAttribute | HierarchyNode | Typed attributes (location, phone, capacity) for ABAC evaluation |
| Permission | Role | Named permission string resource:action |
| PolicyCondition | AccessPolicy | JSON Schema condition on request context attributes |
4. Value objects
| Value object | Shape | Notes |
|---|---|---|
TenantId | branded ULID ten_<ulid> | |
TenantSlug | [a-z0-9-]{3,100} globally unique | Immutable once set |
NodeId | branded ULID nod_<ulid> | |
NodeType | enum organization | facility | department | ward | bed | |
TenantStatus | enum pending | active | suspended | terminated | |
SubscriptionTier | enum STARTER | PROFESSIONAL | ENTERPRISE | |
CountryCode | ISO 3166-1 alpha-2 | |
HierarchyProfileId | string AFG_MOPH | UAE_DOH | PRIVATE_HOSPITAL | Drives node type constraints |
RoleCode | string (tenant-scoped) | Built-ins: TENANT_ADMIN, CLINICIAN, NURSE, PATIENT |
PermissionString | {resource}:{action} | e.g., patient_chart:read |
5. Domain events
All events use platform EventEnvelope; subject format tenant.{aggregate}.{event}.v1.
5.1 Tenant lifecycle
| Event | Subject | Fired by |
|---|---|---|
| TenantCreated | tenant.tenant.created.v1 | CreateTenant |
| TenantActivated | tenant.tenant.activated.v1 | ActivateTenant saga |
| TenantSuspended | tenant.tenant.suspended.v1 | SuspendTenant |
| TenantReactivated | tenant.tenant.reactivated.v1 | ReactivateTenant |
| TenantTerminated | tenant.tenant.terminated.v1 | TerminateTenant |
| TenantUpdated | tenant.tenant.updated.v1 | UpdateTenant |
| TenantConfigChanged | tenant.config.changed.v1 | SetConfig |
| SubscriptionUpdated | tenant.subscription.updated.v1 | UpdateSubscription |
| SubscriptionExpired | tenant.subscription.expired.v1 | Cron job |
5.2 Hierarchy
| Event | Subject | Fired by |
|---|---|---|
| HierarchyNodeCreated | tenant.hierarchy_node.created.v1 | CreateNode |
| HierarchyNodeUpdated | tenant.hierarchy_node.updated.v1 | UpdateNode |
| HierarchyNodeArchived | tenant.hierarchy_node.archived.v1 | ArchiveNode |
5.3 Membership and RBAC
| Event | Subject | Fired by |
|---|---|---|
| UserProfileCreated | tenant.user_profile.created.v1 | CreateUserProfile / JIT from identity.user.registered.v1 |
| OrgMembershipCreated | tenant.org_membership.created.v1 | AssignMembership |
| OrgMembershipRemoved | tenant.org_membership.removed.v1 | RemoveMembership |
| RoleAssigned | tenant.role_assignment.created.v1 | AssignRole |
| RoleRevoked | tenant.role_assignment.removed.v1 | RevokeRole |
| UserInvited | tenant.user.invited.v1 | InviteUser |
6. Ubiquitous language
| Term | Meaning |
|---|---|
| Tenant | The legal entity (hospital system, clinic network) that signs the contract |
| HierarchyNode | An org unit in the tenant's tree (organization, facility, department, ward, bed) |
| HierarchyProfile | A template defining allowed node types and parent constraints for a jurisdiction |
| RootNode | The top-level organization node auto-created on tenant activation |
| Role | A named set of permissions scoped to a tenant; can be assigned at a node |
| RoleAssignment | A binding of (User, Role, Node) |
| OrgMembership | A user's association to a specific node (may hold multiple) |
| UserProfile | Clinical identity (name, specialty, credentials) linked to an identity-service User |
| evaluate() | The RBAC/ABAC decision endpoint that takes a context and returns allow/deny |
| AccessPolicy | An ABAC policy with named conditions evaluated against request context attributes |
| Subscription tier | Commercial entitlement level (STARTER / PROFESSIONAL / ENTERPRISE) |
7. Aggregate relationships
8. Invariants
| # | Invariant |
|---|---|
| INV-01 | tenant.slug must be globally unique (enforced by UNIQUE constraint) |
| INV-02 | A tenant can only be activated once; pending → active is a one-way gate |
| INV-03 | Exactly one root_node_id per active tenant; created during activation saga |
| INV-04 | terminated status is terminal; no lifecycle transitions out of it |
| INV-05 | A HierarchyNode parent must belong to the same tenant |
| INV-06 | Built-in roles (TENANT_ADMIN, CLINICIAN, NURSE, PATIENT) cannot be deleted |
| INV-07 | RoleAssignment requires the user to have an OrgMembership at the same or ancestor node |
| INV-08 | Trial subscriptions must have subscription_end set |
| INV-09 | Tenant configuration keys must be in the platform allow-list |
| INV-10 | Cross-tenant data access is prohibited; all queries scoped by tenant_id |
9. Open questions
- Should ABAC
AccessPolicyevaluation cache invalidate per policy change or per affected user set? - SCIM 2.0 endpoint surface — tenant-service owns the SCIM provisioning or is it a bridge in identity-service?