LearnStack.Hub.Modules.TenantLifecycle owns the Hub-side mirror of LearnStack's Tenant aggregate. It is the root of the Hub domain graph: every subscription, entitlement, custom domain, compliance policy, and usage aggregate hangs off a LearnStackTenant.
Authoritative sources: ADR-0019 § Hub data model, Architecture 24 § 2 ERD.
The name is deliberate — it is the Hub's mirror of the LearnStack-side Tenant, not a second source of truth. Hub is authoritative for plan-related fields; LearnStack core is authoritative for operational fields. The mirror carries metadata only — never tenant content.
LearnStackTenant : AuditableEntity<LearnStackTenantId> (Hub holds it mutably; status transitions + last-phone-home updates mutate it).
| Column | Type | Notes |
|---|---|---|
id |
uuid PK | Mirror of the LearnStack-side tenant.id — same UUID on both sides. Minted by Hub at provisioning and pushed to LearnStack via POST /api/internal/tenants (P02c-3). Use app-side Guid.CreateVersion7() via IGuidFactory so Hub has the id before flush. |
slug |
text | unique; ux_tenants_slug |
display_name |
text | |
status |
text | TenantStatus enum (see below); ck_tenants_status check constraint |
deployment_mode |
text | DeploymentMode enum: SaaS | Dedicated | SelfHostedOnline | SelfHostedAirGapped; ck_tenants_deployment_mode |
last_phone_home_at |
timestamptz null | updated by phone-home (P02c-6); null in P02c-1 |
| audit columns | created_at/by, updated_at/by (OperatorId), deleted_at/by, version from AuditableEntity |
Strongly-typed id: LearnStackTenantId — [ValueObject<Guid>(LearnStackHubVogenDefaults.IdMask)].
Note on
Developmentmode: LearnStack'sDeploymentModeenum has five values includingDevelopment, but a tenant'sdeployment_modeonly takes the four production values above (Developmentis a host-composition concern, not a tenant attribute). The check constraint lists the four.
┌──────────┐
create ──────►│ Trial │
└────┬─────┘
│ activate (subscription → Active)
▼
┌──────────┐ suspend ┌───────────┐
│ Active │────────────►│ Suspended │
│ │◄────────────│ │
└────┬─────┘ reactivate └─────┬─────┘
│ archive │ archive
▼ ▼
┌──────────┐ ┌──────────┐
│ Archived │ │ Archived │
└────┬─────┘ └──────────┘
│ terminate (hard delete, P02c later)
▼
┌────────────┐
│ Terminated │
└────────────┘
Values: Trial | Active | Suspended | Archived | Terminated.
Transition rules enforced as aggregate methods (each returns Result and emits a domain event; invalid transitions return Result.Fail, never throw DomainException):
Activate()—Trial | Suspended → Active. EmitsTenantActivatedDomainEvent.Suspend(reason)—Active → Suspended. EmitsTenantSuspendedDomainEvent.Archive()—Active | Suspended → Archived. EmitsTenantArchivedDomainEvent.Terminate()—Archived → Terminated. EmitsTenantTerminatedDomainEvent. (The hard-delete-with-confirmation flow lands in P02c-4, which builds the operator surface that confirms it; P02c-1 only needs the status transition.)RecordPhoneHome(at)— setslast_phone_home_at(no status change). P02c-6 caller; method shape ships now.
Each status change is a trigger for entitlement recompute when it affects the projection's status/tier (Activate, Suspend) — the handler calls the Entitlements projection service after a successful transition. See ../architecture/entitlement-projection.md.
Application.Contracts:
CreateTenantCommand { Slug, DisplayName, DeploymentMode, InitialPlanId }→Result<TenantCreatedDto>. Orchestrates: createLearnStackTenant(statusTrial) + create initialHubSubscription(Trial) + trigger initialEntitlementrecompute (generation = 1). ThePOST /api/internal/tenantspush to LearnStack core is P02c-3 — in P02c-1 the command stops at Hub-side persistence.ActivateTenantCommand { TenantId }→Result.SuspendTenantCommand { TenantId, Reason }→Result.ArchiveTenantCommand { TenantId }→Result.GetTenantQuery { TenantId }→Result<TenantDetailDto>.ListTenantsQuery { Status?, DeploymentMode?, Cursor, Limit }→Result<Page<TenantSummaryDto>>(cursor pagination).
Validators (FluentValidation): slug format (lowercase, dash-separated, length), display name non-empty, deployment mode in enum.
TenantLifecycleDbContext — hub schema, table tenants. HasDefaultSchema("hub"). Registers the LearnStackTenantId Vogen converter via the shared RegisterVogenIds helper. No RLS, no global query filter (see ../architecture/module-topology.md § Hub does NOT use RLS).
EF config: id PK, ux_tenants_slug unique index, ck_tenants_status + ck_tenants_deployment_mode check constraints, version mapped as the optimistic-concurrency token.
The Hub operator-audit pipeline lands in P02c-4 (LearnStack.Hub.Modules.Audit). Until then, the MUST-audit operations are documented here so the matrix is ready:
| Operation | Class | Snapshot |
|---|---|---|
CreateTenantCommand |
MUST | after |
ActivateTenantCommand |
MUST | before/after status |
SuspendTenantCommand |
MUST | before/after status + reason |
ArchiveTenantCommand |
MUST | before/after status |
TerminateTenantCommand |
MUST | before/after status |
GetTenantQuery / ListTenantsQuery |
MAY | — |
- The
POST /api/internal/tenantspush to LearnStack core (P02c-3). - Keycloak
learnstackrealm provisioning of the tenant admin (P02c-3 / Phase 03). - Hard-delete-with-confirmation termination flow — P02c-4.
- Phone-home
last_phone_home_atupdates from a live caller (P02c-6).