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 case | Command | Triggered by | Emits |
|---|---|---|---|
CreateTenantUseCase | CreateTenantCommand | Super Admin POST | tenant.tenant.created.v1 |
ActivateTenantUseCase | ActivateTenantCommand | Super Admin POST (saga) | tenant.tenant.activated.v1 |
SuspendTenantUseCase | SuspendTenantCommand | Super Admin POST | tenant.tenant.suspended.v1 |
ReactivateTenantUseCase | ReactivateTenantCommand | Super Admin POST | tenant.tenant.reactivated.v1 |
TerminateTenantUseCase | TerminateTenantCommand | Super Admin POST | tenant.tenant.terminated.v1 |
UpdateTenantUseCase | UpdateTenantCommand | Tenant Admin PATCH | tenant.tenant.updated.v1 |
SetTenantConfigUseCase | SetConfigCommand | Tenant Admin PUT | tenant.config.changed.v1 |
UpdateSubscriptionUseCase | UpdateSubscriptionCommand | Super Admin PATCH | tenant.subscription.updated.v1 |
CreateNodeUseCase | CreateNodeCommand | Tenant Admin POST | tenant.hierarchy_node.created.v1 |
ArchiveNodeUseCase | ArchiveNodeCommand | Tenant Admin POST | tenant.hierarchy_node.archived.v1 |
AssignMembershipUseCase | AssignMembershipCommand | Tenant Admin POST | tenant.org_membership.created.v1 |
RemoveMembershipUseCase | RemoveMembershipCommand | Tenant Admin DELETE | tenant.org_membership.removed.v1 |
AssignRoleUseCase | AssignRoleCommand | Tenant Admin POST | tenant.role_assignment.created.v1 |
RevokeRoleUseCase | RevokeRoleCommand | Tenant Admin DELETE | tenant.role_assignment.removed.v1 |
InviteUserUseCase | InviteUserCommand | Tenant Admin POST | tenant.user.invited.v1 |
CreateUserProfileUseCase | CreateUserProfileCommand | JIT on identity.user.registered.v1 | tenant.user_profile.created.v1 |
2. Use cases (queries)
| Query | Returns | Cache strategy |
|---|---|---|
GetTenantQuery | Tenant + config | Redis 5 min key tenant:{id}:meta |
ListTenantsQuery | Paginated list | No cache |
GetHierarchyTreeQuery | Subtree from node | Redis 5 min |
GetNodeAncestorsQuery | Ancestor chain for node | Redis 5 min; used by identity licensing resolver |
EvaluateAccessQuery | { decision: allow|deny, reasons } | No cache (stateless per-request ABAC) |
GetUserProfileQuery | Profile + memberships + roles | Redis 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:
- Query
tenants WHERE status='active' AND subscription_end <= CURRENT_DATE. - For each: emit
tenant.subscription.expired.v1via outbox. - identity-service reacts to downgrade to always-on modules only.
5. Ports
| Port | Implementation |
|---|---|
TenantRepository | DrizzleTenantRepository |
HierarchyNodeRepository | DrizzleHierarchyNodeRepository |
UserProfileRepository | DrizzleUserProfileRepository |
OrgMembershipRepository | DrizzleOrgMembershipRepository |
RoleRepository | DrizzleRoleRepository |
ConfigRepository | DrizzleConfigRepository |
EventPublisher | NatsOutboxEventPublisher |
IdentityServiceClient | HttpIdentityServiceClient |
FacilityServiceClient | HttpFacilityServiceClient |
LicensingServiceClient | HttpLicensingServiceClient |
CachePort | RedisCacheAdapter |
6. Inbox consumers
| Incoming event | Consumer class | Action |
|---|---|---|
identity.user.registered.v1 | UserRegisteredConsumer | JIT create UserProfile for tenant-member users |
identity.user.suspended.v1 | UserSuspendedConsumer | Suspend all OrgMembership for user |
identity.user.deactivated.v1 | UserDeactivatedConsumer | Remove memberships; anonymize profile |
7. Error codes
| Code | HTTP | Meaning |
|---|---|---|
TENANT_NOT_FOUND | 404 | Tenant not found |
TENANT_SLUG_DUPLICATE | 409 | Slug already taken globally |
TENANT_INVALID_TRANSITION | 422 | State machine constraint violated |
TENANT_CONFIG_KEY_UNKNOWN | 400 | Key not in allow-list |
TENANT_CROSS_TENANT | 403 | Cross-tenant data access attempt |
TENANT_ACTIVATION_FAILED | 500 | Saga exhausted retries |
TENANT_NODE_CROSS_TENANT | 422 | Parent node belongs to different tenant |
TENANT_ROLE_NOT_FOUND | 404 | Role code not defined for tenant |
TENANT_MEMBERSHIP_REQUIRED | 422 | Role assignment requires membership at node |