Skip to main content

Tenant Service — Application Logic

Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · DOMAIN_MODEL · API_CONTRACTS

1. Use cases (commands)

Use caseCommandTriggered byEmits
CreateTenantUseCaseCreateTenantCommandSuper Admin POSTtenant.tenant.created.v1
ActivateTenantUseCaseActivateTenantCommandSuper Admin POST (saga)tenant.tenant.activated.v1
SuspendTenantUseCaseSuspendTenantCommandSuper Admin POSTtenant.tenant.suspended.v1
ReactivateTenantUseCaseReactivateTenantCommandSuper Admin POSTtenant.tenant.reactivated.v1
TerminateTenantUseCaseTerminateTenantCommandSuper Admin POSTtenant.tenant.terminated.v1
UpdateTenantUseCaseUpdateTenantCommandTenant Admin PATCHtenant.tenant.updated.v1
SetTenantConfigUseCaseSetConfigCommandTenant Admin PUTtenant.config.changed.v1
UpdateSubscriptionUseCaseUpdateSubscriptionCommandSuper Admin PATCHtenant.subscription.updated.v1
CreateNodeUseCaseCreateNodeCommandTenant Admin POSTtenant.hierarchy_node.created.v1
ArchiveNodeUseCaseArchiveNodeCommandTenant Admin POSTtenant.hierarchy_node.archived.v1
AssignMembershipUseCaseAssignMembershipCommandTenant Admin POSTtenant.org_membership.created.v1
RemoveMembershipUseCaseRemoveMembershipCommandTenant Admin DELETEtenant.org_membership.removed.v1
AssignRoleUseCaseAssignRoleCommandTenant Admin POSTtenant.role_assignment.created.v1
RevokeRoleUseCaseRevokeRoleCommandTenant Admin DELETEtenant.role_assignment.removed.v1
InviteUserUseCaseInviteUserCommandTenant Admin POSTtenant.user.invited.v1
CreateUserProfileUseCaseCreateUserProfileCommandJIT on identity.user.registered.v1tenant.user_profile.created.v1

2. Use cases (queries)

QueryReturnsCache strategy
GetTenantQueryTenant + configRedis 5 min key tenant:{id}:meta
ListTenantsQueryPaginated listNo cache
GetHierarchyTreeQuerySubtree from nodeRedis 5 min
GetNodeAncestorsQueryAncestor chain for nodeRedis 5 min; used by identity licensing resolver
EvaluateAccessQuery{ decision: allow|deny, reasons }No cache (stateless per-request ABAC)
GetUserProfileQueryProfile + memberships + rolesRedis 5 min
GetTenantStatusQuery{ status, rootNodeId }Redis 1 min (internal probe)

3. Activation saga

Failure handling: Bounded exponential backoff (3 attempts, 1s/2s/4s delays). On exhaustion: tenant stays PENDING; alert fires; partial effects are idempotent (create-or-return).

4. Subscription expiry cron

SubscriptionExpiryJob runs daily at 00:05 UTC:

  1. Query tenants WHERE status='active' AND subscription_end <= CURRENT_DATE.
  2. For each: emit tenant.subscription.expired.v1 via outbox.
  3. identity-service reacts to downgrade to always-on modules only.

5. Ports

PortImplementation
TenantRepositoryDrizzleTenantRepository
HierarchyNodeRepositoryDrizzleHierarchyNodeRepository
UserProfileRepositoryDrizzleUserProfileRepository
OrgMembershipRepositoryDrizzleOrgMembershipRepository
RoleRepositoryDrizzleRoleRepository
ConfigRepositoryDrizzleConfigRepository
EventPublisherNatsOutboxEventPublisher
IdentityServiceClientHttpIdentityServiceClient
FacilityServiceClientHttpFacilityServiceClient
LicensingServiceClientHttpLicensingServiceClient
CachePortRedisCacheAdapter

6. Inbox consumers

Incoming eventConsumer classAction
identity.user.registered.v1UserRegisteredConsumerJIT create UserProfile for tenant-member users
identity.user.suspended.v1UserSuspendedConsumerSuspend all OrgMembership for user
identity.user.deactivated.v1UserDeactivatedConsumerRemove memberships; anonymize profile

7. Error codes

CodeHTTPMeaning
TENANT_NOT_FOUND404Tenant not found
TENANT_SLUG_DUPLICATE409Slug already taken globally
TENANT_INVALID_TRANSITION422State machine constraint violated
TENANT_CONFIG_KEY_UNKNOWN400Key not in allow-list
TENANT_CROSS_TENANT403Cross-tenant data access attempt
TENANT_ACTIVATION_FAILED500Saga exhausted retries
TENANT_NODE_CROSS_TENANT422Parent node belongs to different tenant
TENANT_ROLE_NOT_FOUND404Role code not defined for tenant
TENANT_MEMBERSHIP_REQUIRED422Role assignment requires membership at node