Skip to main content

4E Control Plane

Source code: backend/Applications/4EOrganizationsManager/ Projects: FourE.OrganizationsManager.{Utility, Entity, Common, Api, Workers, Host, Tests} Runtime: .NET 10 + EF Core + PostgreSQL. Its own database (four_e_organizations, schema four_e), its own Redis, its own RabbitMQ, its own deployable process.

In plain words

Imagine a big apartment building full of separate flats. Each flat is one customer's ERP system — their own private space with their own furniture (their data) that nobody else can see. 4E is the building's management office. It is not one of the flats. It is the front desk and the maintenance room.

When a new family wants to move in, the office:

  • signs them up and writes them into the tenant register,
  • decides which wing of the building they go in (a "cell"),
  • hands them their keys and access cards (their login accounts and API keys),
  • ticks off which rooms/amenities they paid for (which ERP "modules" they may use),
  • collects the rent every month (billing through Stripe),
  • keeps a logbook of everything that happens (events, incidents, failures),
  • and lets a building superintendent step into a flat for a short, recorded visit when the family needs help (impersonation).

The families never walk into the management office to run their own home, and the office never lives inside anyone's flat. It just keeps the whole building organised.

The business problem 4E solves. A SaaS ERP has to create, route, meter, bill and govern many isolated customer tenants from one operator console — without ever leaking one customer's data or configuration into another, and without each customer's ERP needing to know how it was created or how it gets paid for. 4E is the one place that does all of this. It feeds the customer's actual ERP runtime only two things: "yes, this tenant is allowed to be online right now" (routability) and "here are the modules they are entitled to use" (entitlements).

A company would use this to: sign a customer up (self-serve public signup → Stripe Checkout → automated provisioning, or operator-created), give that customer a working tenant ERP in the right region, turn modules on and off as their subscription changes, suspend / restore / archive / move / clone / delete tenants, invite the first admin user, watch the health of the whole fleet, and let support staff issue a short-lived, read-only "look but don't touch" token into a customer's system.

Define the words once
  • Tenant = one customer's running ERP instance (one "flat"). The unit of isolation.
  • Organization = the legal customer entity that owns one or more tenants (the "family name on the lease").
  • Cell = a physical placement target / wing of the building where tenants run (a region + a database target).
  • Module entitlement = a record saying "this tenant may run this ERP module" (which amenities are paid for).
  • Provisioning = the automated process of standing up a brand-new tenant runtime.
  • Control plane = the management layer that governs tenants but does not run the tenants' business logic. (The thing that runs the business logic is the runtime, a separate product.)
  • RLS (Row-Level Security) = a database safety net where the database itself hides rows that don't belong to your tenant. 4E deliberately has none — see the gaps section.

What it is responsible for

4E is the operator-global control plane. "Operator-global" means it sees every tenant at once (it is the building manager, not a resident), so it has no per-tenant session and no Row-Level Security. Isolation is enforced by an explicit TenantId foreign key on every row plus authorization policies — never by a database query filter.

Owns (it is the authoritative source of truth)

AreaTables
Organization registryorganizations
Cell registry + runtime routingcells
Tenant registry + host routingtenants, tenant_domains, tenant_api_keys
Module entitlements (runtime SSOT)module_entitlements
Subscription catalog + pricingsubscription_plans, subscription_plan_modules, pricing_tiers
Provisioning sagaprovisioning_jobs, provisioning_job_steps
External-binding evidencekeycloak_provisioning_records, migration_runs
Operation audittenant_operations
Control-plane spinecontrol_plane_events, control_plane_health_snapshots, control_plane_dead_letters
Self-serve billingbilling_prospects, billing_subscriptions, billing_invoices, billing_dunning_cases, billing_webhook_deliveries, billing_checkout_sessions
Support impersonationimpersonation_grants
Owner identity + executive portalowner_profiles, company_groups, company_group_members, owner_company_launch_tokens
Tenant-user invitationstenant_user_invitations

Delegates (it does NOT own these)

  • The ERP runtime itself (ERP.Api + its modules) — 4E only resolves routing + entitlements for it.
  • Auth / record-rule evaluation / company-scope — Auth stays the runtime record-rule authority. 4E integrates no record-rule evaluator; it provides routability + entitlement evidence that Auth consumes.
  • Credential storage — Keycloak is the credential authority. owner_profiles is an identity projection only, with no secrets.
  • Actual module DB migrations — executed by a reviewed-bundle runner against the runtime DBs; 4E only records evidence and pins catalog heads.
  • Payment processing — Stripe charges the card; 4E persists and mirrors, it never charges directly.
  • Runtime owner company-access reads — delegated to the runtime via IRuntimeOwnerAccessProbe (4E has no tenant RLS session, so it cannot read the runtime's UserCompanyAccess directly).

Code structure

The module follows the mandated ERP.Next layering. Reading bottom-up: Utility is a dependency-free leaf, Entity is EF Core only, Common holds all the CQRS application logic, Api is thin HTTP boundaries, Workers runs background processors + external clients, Host is the deployable composition root, and Tests covers them all.

4EOrganizationsManager/
├── FourE.OrganizationsManager.Utility/ # leaf: constants, errors, options, money, redactor (no deps)
│ ├── OrganizationsManagerConstants.cs # Schema="four_e", max-lengths, ProvisioningSagaSteps, event/op-type names
│ ├── OrganizationsManagerErrors.cs # canonical Error codes
│ ├── OrganizationsManagerPolicies.cs # operator/executive/runtime authorization policy names
│ ├── OrganizationsManagerRateLimitPolicies.cs # PublicSignup / StripeWebhook / PublicCatalog / IdentityInvite limits
│ ├── MinorUnitMoney.cs # the ONLY major↔minor currency conversion site
│ └── ProvisioningSecretRedactor.cs # redacts secrets out of failure messages

├── FourE.OrganizationsManager.Entity/ # EF Core: POCO models, configs, DbContext, migrations, seeds
│ ├── Models/ # 29 tables' POCOs + status enums
│ ├── Configurations/ # 30 IEntityTypeConfiguration classes (mapping/index/check-constraint)
│ ├── Context/OrganizationsManagerDbContext.cs # 30 DbSets; NO query filters / NO RLS (by design)
│ ├── Migrations/ # 25 reviewed migrations + model snapshot (never auto-applied)
│ └── Seeds/SubscriptionPlanSeeder.cs # seeds System plans + backfills entitlement PlanId

├── FourE.OrganizationsManager.Common/ # application logic: CQRS feature folders + service facades
│ ├── Services/OrganizationsManagerService.cs # the big typed facade dispatching to handlers
│ ├── Organizations/ Cells/ Tenants/ ModuleEntitlements/ Plans/ PricingTiers/ # operator CQRS
│ ├── ProvisioningJobs/Saga/Steps/ # 9 step executors + ModuleMigrationCatalog + ControlPlaneMessages
│ ├── Billing/ Checkout/ PublicSignup/ PublicKyc/ PublicCatalog/ # self-serve revenue path + Stripe handlers
│ ├── TenantUsers/ # invite / provision-primary-admin / grant-verified-admin-role
│ ├── Impersonation/ # issue / revoke / validate / search + token signer
│ ├── DeadLetters/ Monitoring/ TenantOperations/ # audit, monitoring, DLQ reads + writes
│ ├── Owners/ Portal/ # cross-tenant owner identity + executive portal + launch tokens
│ ├── ApiKeys/ MigrationRuns/ Keycloak/ # API-key issue/revoke; migration-run record; Keycloak ports
│ └── Behaviors/ Shared/ # validation pipeline behavior + shared dispatcher helpers

├── FourE.OrganizationsManager.Api/Controllers/ # 18 thin HTTP boundary controllers

├── FourE.OrganizationsManager.Workers/ # background processors + external clients
│ ├── Provisioning/ # ProvisioningSaga*, DeadLetterIntake, PendingAdminRoleGrantSweep,
│ │ # RabbitMq publisher/replayer/topology, RedisTenantRuntimeResolutionCache
│ ├── Keycloak/ # KeycloakAdminClient / KeycloakUserAdminClient + token providers
│ ├── Stripe/ # HttpStripeBillingClient + StripeWebhookEventDispatchWorker
│ └── RuntimeAccess/HttpRuntimeOwnerAccessProbe.cs # config-gated 4E→runtime owner-access probe

├── FourE.OrganizationsManager.Host/ # composition root: the deployable API + worker process
│ ├── Program.cs # builds host; wires auth/rate-limit/workers/Stripe/OTel; --seed-billing-catalog CLI
│ └── FourEHostSecurityExtensions.cs # multi-audience auth + the operator policy ladder

└── FourE.OrganizationsManager.Tests/ # Api / Application / Architecture / Money / Persistence / Security / Workers
CQRS in one breath

Every write goes Controller → IOrganizationsManagerService facade (or a feature service) → Command + Handler + FluentValidation Validator. Every read is a Query + Handler + Validator. Controllers never contain business logic — they are HTTP plumbing only.

How it works

The headline runtime flow is standing up a brand-new paid tenant: a stranger on the public website turns into a fully provisioned, online ERP instance, with nobody from the operator team touching a keyboard. It chains the self-serve revenue path (signup → Stripe Checkout → webhook) into the provisioning saga.

Walkthrough for a beginner.

  1. A visitor fills in the public signup form (POST /api/public/signup, anonymous + rate-limited). 4E creates a checkout session row and asks Stripe to make a hosted payment page.
  2. The visitor pays on Stripe's page. 4E never sees the card.
  3. Stripe calls back over HTTPS to POST /api/public/webhooks/stripe. 4E first verifies the HMAC signature (proves it's really Stripe), then drops the raw event into the billing_webhook_deliveries inbox with status Pending. The HTTP request returns immediately — no business work happens on the web thread.
  4. A background worker, StripeWebhookEventDispatchWorker, polls the database for Pending / Failed rows and hands each to the dispatcher. (It is a DB poller, not a broker consumer — important for understanding retries.)
  5. For checkout.session.completed, the dispatcher routes to ProvisionPaidTenantHandler, which is idempotent on the Stripe subscription id: it creates the organization + tenant + entitlement graph and enqueues a provisioning job. Running it twice cannot create two tenants.
  6. The provisioning saga (ProvisioningSagaRunner, driven by ProvisioningSagaBackgroundService) leases the next Pending job using the lease-poll index, then runs the 9 ordered steps below. The lease is re-checked before and after every step; a stolen/expired lease fails loud with a Conflict.
  1. Each step is an IProvisioningStepExecutor recorded in provisioning_job_steps. Succeeded steps are skipped on resume — so a crashed-and-restarted job picks up where it left off instead of redoing Keycloak work. A step failure marks the step Failed and aborts the job.
  2. The rabbit-event-publish step emits tenant-runtime-provisioned.v1 on RabbitMQ; the runtime's entitlement feed consumes it. The final tenant-activation step flips the tenant to Active (only if it is still Provisioning/Restoring). From then on, the runtime can resolve and route the tenant.
Why an inbox + a saga instead of "just do it inline"?

Two reasons. The inbox (step 3) makes Stripe webhooks idempotent and replayable — Stripe retries aggressively, and the unique index on (Provider, ProviderEventId) means a duplicate is recorded as Duplicate instead of double-charging anyone. The saga (step 6) makes a multi-system operation (Keycloak + migrations + Redis + RabbitMQ + DB) crash-safe and resumable, because each step's success is durably recorded before the next begins.

Database design

The control-plane database is four_e_organizations, schema four_e, 29 tables, and — uniquely among ASAB modules — zero Row-Level Security. Common conventions: PK Id uuid; a uint RowVersion mapped to PostgreSQL xmin for optimistic concurrency on almost every mutable table; CreatedAtUtc / UpdatedAtUtc as timestamptz; enums persisted as strings via HasConversion<string> (a small number as int).

Registry & routing

organizations — the customer / legal entity.

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
Namevarchar(200)not nullLegal/display name
Slugvarchar(80)not null, uniqueURL-safe handle
CountryCodevarchar(8)not nullISO country code
CountryNamevarchar(120)not nullDisplay country
Statusvarchar(32)not null, default ActiveActive / Suspended / Archived
CreatedAtUtctimestamptznot nullCreated
UpdatedAtUtctimestamptznot nullLast changed
RowVersionxminconcurrencyOptimistic lock

cells — an infra placement target ("wing").

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
Codevarchar(80)not null, uniqueStable cell code
Namevarchar(160)not nullDisplay name
RegionCodevarchar(32)not nullGeographic region
RuntimeDatabaseTargetvarchar(500)not nullRouting metadata (no secrets)
Statusvarchar(32)not null, default ActiveActive / Draining / Offline
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

tenants — the ERP instance unit ("flat").

ColumnTypeKey/ConstraintsMeaning
IduuidPK (also alt key (Id, TenantId))Identity
OrganizationIduuidFK→organizations, not null, RESTRICTOwning org
CellIduuidFK→cells, not null, RESTRICTPlacement
Namevarchar(200)not nullDisplay name
Slugvarchar(80)not null, globally uniqueRouting handle
VerticalTypevarchar(32)nullable, col vertical_typefactory / insurance-broker (NOT NULL deferred)
Statusvarchar(32)not null, default ProvisioningProvisioning / Active / Suspended / Restoring / Failed / Archived
ObjectStoragePrefixvarchar(160)not nullStorage namespace
PrimaryContactEmailvarchar(254)nullableContact
PrimaryContactNamevarchar(200)nullableContact
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

tenant_domains — host → tenant resolution.

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantIduuidFK→tenants, not null, CASCADEOwning tenant
HostNamevarchar(255)not null, globally uniqueHostname routed to this tenant
IsVerifiedboolnot nullOwnership verified
CreatedAtUtctimestamptznot nullCreated
RowVersionxminconcurrencyOptimistic lock

tenant_api_keys — runtime API credentials (hash + prefix only).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantIduuidFK→tenants, not null, CASCADEOwning tenant
Namevarchar(160)not nullKey label
KeyHashvarchar(128)not null, uniqueHash of key (plaintext never stored)
KeyPrefixvarchar(32)not null, partial-uniqueVisible prefix for lookup
RequestedByvarchar(200)not nullIssuer
IdempotencyKeyvarchar(160)not null, UX(TenantId, IdempotencyKey)Idempotent issue
ExpiresAtUtc / RevokedAtUtc / LastUsedAtUtctimestamptznullableLifecycle stamps
CreatedAtUtctimestamptznot nullCreated
RowVersionxminconcurrencyOptimistic lock

Entitlement & catalog

module_entitlements — which modules a tenant may run (the runtime's source of truth).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantIduuidFK→tenants, not null, CASCADEOwning tenant
ModuleCodevarchar(80)not null, UX(TenantId, ModuleCode)Module key
PlanCodevarchar(80)not nullLegacy plan code
PlanIduuidFK→subscription_plans, nullable, RESTRICTPlan link (backfilled; NOT NULL deferred)
Statusvarchar(32)not null, default EnabledRequested / Migrating / Enabled / Failed / Suspended / Disabled
EffectiveFromUtctimestamptznot nullWindow start
EffectiveToUtctimestamptznullableWindow end (termination = Disabled + EffectiveToUtc)
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

subscription_plans — the catalog plan.

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
Codevarchar(80)not null, uniquePlan code
Namevarchar(160)not nullDisplay name
Descriptionvarchar(500)nullableMarketing copy
VerticalTypevarchar(32)nullableVertical, or null = agnostic
Statusvarchar(32)not nullDraft / Active / Retired
PlanKindvarchar(32)not null, default PublicPublic / System (System = seeder-owned, never public)
TrialDaysintnot null, CHECK 0..365Free-trial length
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

subscription_plan_modules — plan ↔ module mapping.

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
SubscriptionPlanIduuidFK→subscription_plans, not null, CASCADEPlan
ModuleCodevarchar(80)not null, UX(PlanId, ModuleCode)Module key
IsIncludedboolnot null, default trueIn the plan?
DisplayOrderintnot null, IX(PlanId, DisplayOrder)Sort
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

pricing_tiers — a priced tier of a plan (money in minor units, e.g. cents).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
SubscriptionPlanIduuidFK→subscription_plans, not null, CASCADEPlan
Codevarchar(80)not null, UX(PlanId, Code)Tier code
Namevarchar(160)not nullDisplay name
CurrencyCodevarchar(3)not nullISO currency
BillingIntervalvarchar(32)not nullMonthly / Yearly
UnitAmountMinorbigintnot null, CHECK ≥0Base price in minor units
StripePriceIdvarchar(255)nullable, partial-uniqueStripe price for webhook mapping
PerSeatAmountMinorbigintnullable, CHECK null or ≥0Per-seat add-on
IncludedSeats / MinimumSeatsintnot null, CHECK boundsSeat policy
MaximumSeatsintnullable, CHECK null or ≥ MinimumSeat cap
IsActiveboolnot null, default trueSellable?
DisplayOrderintnot nullSort
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

Index of note: UX(PlanId, BillingInterval, CurrencyCode) WHERE IsActive — exactly one active tier per plan/interval/currency.

Provisioning

provisioning_jobs — the leased, idempotent saga job.

ColumnTypeKey/ConstraintsMeaning
IduuidPK (also alt key (Id, TenantId))Identity
TenantIduuidFK→tenants, not null, CASCADETarget tenant
OperationTypevarchar(80)not nulle.g. create-tenant-runtime / move / enable-module-runtime
RequestedByvarchar(200)not nullRequestor
Reasonvarchar(500)not nullAudit reason
IdempotencyKeyvarchar(160)not null, UX(TenantId, IdempotencyKey)Dedupe key
Statusvarchar(32)not null, default PendingPending / Leased / Succeeded / Failed / Cancelled
AttemptCountintnot nullRetry counter
LeaseIdvarchar(80)nullableActive lease token
LeasedByvarchar(160)nullableWorker id
LeaseExpiresAtUtctimestamptznullableLease deadline
CurrentStepCode / FailureCodevarchar(80)nullableProgress / failure tag
FailureMessagevarchar(500)nullableRedacted failure text
CompletedAtUtctimestamptznullableFinished
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

Lease-poll index: IX(Status, LeaseExpiresAtUtc, CreatedAtUtc).

provisioning_job_steps — the resumable per-step ledger.

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
ProvisioningJobId + TenantIduuidcomposite FK→jobs (Id, TenantId), CASCADEOwning job
Sequenceintnot null, UX(JobId, Sequence)Order
StepCodevarchar(80)not null, UX(JobId, StepCode)One of the 9 step codes
Statusvarchar(32)not null, default PendingPending / Succeeded / Failed
FailureCode / FailureMessagevarchar(80) / varchar(500)nullableFailure detail
StartedAtUtctimestamptznot nullStep start
CompletedAtUtctimestamptznullableStep end
UpdatedAtUtctimestamptznot nullLast change
RowVersionxminconcurrencyOptimistic lock

keycloak_provisioning_records — external-resource bindings (IDs / fingerprints only, never secrets).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantIduuidFK→tenants, not null, RESTRICTTenant
ResourceTypevarchar(80)not null, UX(TenantId, ResourceType, ResourceKey)org / client / role / user
ResourceKeyvarchar(200)not nullDeterministic key
ExternalIdvarchar(200)not nullKeycloak id
Statusvarchar(32)not nullSucceeded / Replayed / PendingVerification / Provisioned / Skipped
Fingerprintvarchar(128)not nullDrift detection (fails loud, no silent rebind)
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

migration_runs — reviewed module-migration evidence (no RowVersion; append-only).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantIduuidFK→tenants, not null, CASCADETenant
ModuleCodevarchar(80)not nullModule
MigrationAssemblyvarchar(160)not nullBundle assembly
MigrationIdvarchar(160)not null, UX(TenantId, ModuleCode, MigrationAssembly, MigrationId)Migration id
ScriptHashvarchar(128)not nullReviewed-bundle hash (drift fails loud)
TargetDatabase / TargetSchemavarchar(200)not nullWhere it ran
Outcomevarchar(40)not nullResult
StartedAtUtctimestamptznot nullStart
CompletedAtUtctimestamptznullableEnd
RequestedByvarchar(200)not nullRequestor
JobIduuidnullableLinked provisioning job
FailureReasonvarchar(200)nullableFailure text
AppliedAtUtctimestamptznot nullApplied

tenant_operations — lifecycle/op audit ledger (Status stored as int).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantIduuidnot null (row-scope key, no nav FK)Subject tenant
CellIduuidFK→cells, nullable, SetNullCell context
TargetTenantIduuidnullableFor move / clone
OperationTypevarchar(80)not nullOperation name
ModuleCodevarchar(80)nullableModule context
IdempotencyKeyvarchar(160)nullable, partial-UX WHERE not nullDedupe
ProvisioningJobIduuidcomposite FK→jobs, nullable, SetNullLinked job
LeaseIdvarchar(80)nullableLease
AttemptintnullableAttempt
MigrationIdvarchar(160)nullableMigration ref
FromStatus / ToStatusvarchar(32)nullableState transition
Statusintnot nullPending=0 / Succeeded=1 / Failed=2
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

Identity / invitation

tenant_user_invitations — control-plane invite intent + idempotency anchor.

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantIduuidFK→tenants, not null, RESTRICTTarget tenant
Emailvarchar(254)not null (normalized lowercase)Invitee
FirstName / LastNamevarchar(100)nullableName
Statusvarchar(32)not nullPending / Accepted / Revoked / Expired
KeycloakUserExternalIdvarchar(200)nullableKeycloak user id once created
RequestedByOperatorvarchar(200)not nullOperator
IdempotencyKeyvarchar(128)not null, uniqueDedupe
CreatedAtUtc / UpdatedAtUtc / ExpiresAtUtctimestamptznot nullTTL default 7 days
RowVersionxminconcurrencyOptimistic lock

Plus a partial-unique (tenant_id, lower(email)) WHERE status='Pending' (raw SQL) — one live invite per address per tenant.

Monitoring / audit / DLQ

control_plane_events — the audit-spine event ledger.

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantIduuidFK→tenants, nullable, RESTRICTTenant scope
CellIduuidFK→cells, nullable, RESTRICTCell scope
ModuleCodevarchar(80)not nullModule
EventTypevarchar(120)not nullEvent name
Severityvarchar(32)not nullInformation / Warning
Summaryvarchar(500)not nullHuman summary
CorrelationIdvarchar(160)not nullTrace id
OccurredAtUtctimestamptznot nullWhen it happened
MetadataJsonvarchar(4000)nullableStructured detail
CreatedAtUtctimestamptznot nullRecorded
RowVersionxminconcurrencyOptimistic lock

CHECK CK_control_plane_event_scope: TenantId or CellId must be set — EXCEPT when ModuleCode='executive-portal-identity' (owner events are legitimately cross-tenant, both null).

control_plane_health_snapshots — fleet health projection.

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantId / CellIduuidFK nullable, RESTRICT, CHECK one setScope
ComponentCodevarchar(80)not nullComponent
ComponentInstancevarchar(160)not nullInstance id
Statusvarchar(32)not nullHealth status
RegionCodevarchar(32)not nullRegion
LatencyMillisecondsintnullable, CHECK null or ≥0Latency
Messagevarchar(500)not nullDetail
ObservedAtUtc / CreatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

control_plane_dead_letters — RabbitMQ DLQ intake (Status stored as int).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
MessageIduuidnot null, uniqueIdempotent intake key
TenantId / CellIduuidFK, not null, RESTRICTScope
OperationId / CorrelationIduuidnot nullTrace ids
IdempotencyKeyvarchar(160)not nullOriginal dedupe key
ModuleCodevarchar(80)not nullModule
EventNamevarchar(120)not nullFailed event
RoutingKeyvarchar(160)not nullRouting key
FailureReasonvarchar(500)not nullWhy it dead-lettered
PayloadJsonvarchar(8000)not nullOriginal payload
Statusintnot nullPending=0 / Retried=1 / Resolved=2
ReplayCountintnot null, CHECK ≥0Replays
LastReplayedAtUtc / ResolvedAtUtctimestamptznullableStamps
ResolvedByvarchar(200)nullableOperator
ResolutionNotevarchar(500)nullableNote
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

Billing (Stripe-aligned, minor-unit money)

billing_prospects — billing-customer projection per tenant.

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantIduuidFK→tenants, RESTRICTTenant
BillingEmailvarchar(254)not nullBilling contact
DisplayNamevarchar(200)not nullCustomer name
CurrencyCodevarchar(3)not nullCurrency
ProviderCustomerIdvarchar(200)nullable, partial-UXStripe customer id
Statusvarchar(32)not nullActive / Archived
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

billing_subscriptions — Stripe subscription mirror.

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantIduuidFK→tenants, RESTRICTTenant
ProspectIduuidFK→prospects, nullable, RESTRICTBilling customer
PlanCodevarchar(80)not nullPlan
BillingIntervalvarchar(32)not nullMonthly / Yearly
Quantityintnot null, CHECK >0Seats
CurrencyCodevarchar(3)not nullCurrency
Statusvarchar(32)not nullPendingProviderActivation / Active / PastDue / Canceled
Providervarchar(32)not nullPayment provider
ProviderSubscriptionIdvarchar(200)nullable, partial-UXStripe sub id
IdempotencyKeyvarchar(160)not null, uniqueDedupe
StartedAtUtctimestamptznot nullStart
CurrentPeriodEndsAtUtc / TrialEndsAtUtctimestamptznullablePeriod ends
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

billing_invoices — invoice (minor units).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantIduuidFK→tenants, RESTRICTTenant
SubscriptionIduuidFK→subscriptions, RESTRICTSubscription
InvoiceNumbervarchar(80)not null, uniqueNumber
Statusvarchar(32)not nullDraft / Open / Paid / Void / Uncollectible
CurrencyCodevarchar(3)not nullCurrency
SubtotalMinor / TaxMinor / TotalMinorbigintnot null, CHECK ≥0Amounts (minor units)
IssuedAtUtctimestamptznot nullIssued
DueAtUtc / PaidAtUtctimestamptznullableDue / paid
Providervarchar(32)not nullProvider
ProviderInvoiceIdvarchar(200)nullable, partial-UXStripe invoice id
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

billing_dunning_cases — failed-payment retry case.

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantIduuidFK→tenants, RESTRICTTenant
SubscriptionIduuidFK→subscriptions, RESTRICTSubscription
InvoiceIduuidFK→invoices, nullable, RESTRICTInvoice
Statusvarchar(32)not nullOpen / Paused / Resolved
AttemptCountintnot null, CHECK ≥0Retries
OpenedAtUtctimestamptznot nullOpened
NextAttemptAtUtc / ResolvedAtUtctimestamptznullableSchedule
LastFailureCodevarchar(120)not nullCode
LastFailureMessagevarchar(500)not nullMessage
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

billing_webhook_deliveries — the Stripe webhook inbox (no TenantId — keyed by provider event).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
Providervarchar(32)not nullProvider
ProviderEventIdvarchar(200)not null, UX(Provider, ProviderEventId)Provider event id
EventTypevarchar(120)not nulle.g. invoice.paid
SignatureHeaderHashvarchar(128)not nullVerified signature hash
PayloadHashvarchar(128)not nullBody hash
PayloadJsontextnullableVerified raw body
ProcessingStatusvarchar(32)not nullAccepted / Duplicate / Rejected + Pending / Processed / Skipped / Failed / DeadLettered
FailureCode / FailureMessagevarchar(120) / varchar(500)nullableFailure
DispatchAttemptCountintdefault 0Async dispatch attempts
ReceivedAtUtc / ProcessedAtUtc / CreatedAtUtctimestamptzmixedTimestamps
RowVersionxminconcurrencyOptimistic lock

billing_checkout_sessions — pre-payment self-serve intent (no tenant yet).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
SignupRequestIdvarchar(160)not null, uniqueClient idempotency UUID
PlanCodevarchar(80)not nullChosen plan
PricingTierIduuidFK→pricing_tiers, RESTRICTChosen tier
BillingIntervalvarchar(32)not nullMonthly / Yearly
Seatsintnot null, CHECK >0Seats
FounderEmailvarchar(254)not nullSignup email
VerticalTypevarchar(32)not nullVertical
CompanyNamevarchar(200)not nullCompany
TenantSlugvarchar(80)not nullDesired slug
CurrencyCodevarchar(3)not nullCurrency
Statusvarchar(32)not nullCheckoutPending / Completed
StripeCheckoutSessionIdvarchar(200)nullable, partial-UXStripe checkout id
CheckoutUrlvarchar(~1020)nullableHosted payment URL
StripeSubscriptionIdvarchar(200)nullable, partial-UXBecomes ProvisionPaidTenant idempotency key
TenantIduuidnullableSet once provisioned
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

Impersonation / owner identity

impersonation_grants — signed, read-only support grant (Status stored as int).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TenantIduuidFK→tenants, RESTRICTTarget tenant
OperatorSubjectvarchar(256)not nullOperator subject
OperatorDisplayNamevarchar(256)not nullOperator name
Reasonvarchar(1024)not nullAudit reason
TokenIduuidnot null, uniqueSigned-token jti
Statusintnot nullIssued=1 / Revoked=2 / Expired=3
IssuedAtUtc / ExpiresAtUtctimestamptznot null, CHECK Expires > IssuedValidity window
RevokedAtUtctimestamptznullableRevoked
CreatedAtUtctimestamptznot nullCreated
RowVersionxminconcurrencyOptimistic lock

owner_profiles — cross-tenant human owner (identity projection only, no secrets).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
KeycloakSubjectIdvarchar(64)col keycloak_subject_id, uniquePer-request hot lookup
Emailvarchar(254)lowercase, lower(email) unique (raw SQL)Owner email
DisplayNamevarchar(200)not nullName
Statusvarchar(32)not nullActive / Suspended / Archived
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

company_groups — an owner's portfolio (V1 invariant: one group per owner).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
OwnerProfileIduuidFK→owner_profiles, RESTRICT, uniqueOne group per owner
Namevarchar(200)not nullPortfolio name
GroupVersionintnot nullMonotonic cache-invalidation token
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

company_group_members — the explicit, auditable owner→tenant link (the ONLY ownership marker).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
CompanyGroupIduuidFK→company_groups, RESTRICTOwner's group
TenantIduuidFK→tenants, RESTRICTLinked tenant
LinkSourcevarchar(32)not nullOperatorLinked / SignupClaimed
LinkStatusvarchar(32)not nullPendingVerification / Active / Revoked
LinkedByActorvarchar(200)not nullWho linked
VerifiedAtUtctimestamptznullableVerified
IdempotencyKeyvarchar(128)not nullDedupe
CreatedAtUtc / UpdatedAtUtctimestamptznot nullTimestamps
RowVersionxminconcurrencyOptimistic lock

Plus partial-unique (tenant_id) WHERE link_status='Active' — one active owner per tenant.

owner_company_launch_tokens — single-use portal → tenant SSO handoff (no RowVersion; single-use).

ColumnTypeKey/ConstraintsMeaning
IduuidPKIdentity
TokenHashvarchar(64)col token_hash, SHA-256 hex, uniqueHash of launch token (raw never stored)
OwnerProfileIduuidFK→owner_profiles, RESTRICTOwner
TenantIduuidFK→tenants, RESTRICTTarget tenant
KeycloakSubjectvarchar(64)col keycloak_subjectSubject
ExpiresAtUtctimestamptznot null, IXTTL 120s
RedeemedAtUtctimestamptznullableSingle-use marker
CreatedAtUtctimestamptznot nullCreated
The authorization ladder

Endpoints are guarded by escalating operator policies defined in OrganizationsManagerPolicies.cs + FourEHostSecurityExtensions.cs: OperatorRead < OperatorSupport < OperatorManage < OperatorAdmin < OperatorOwner (scheme FourEOperator); the executive portal uses ExecutivePortalOwner / FourEPortalExecutive (scheme FourEExecutive); and the tenant runtime authenticates with FourERuntimeService.

Features

Organization lifecycle

Plain: create a customer, edit it, and suspend / restore / archive it.

  • CQRS: CreateOrganizationHandler, GetOrganizationHandler, SearchOrganizationsHandler, UpdateOrganizationHandler, OrganizationLifecycleHandler, SearchOrganizationTenantsHandler.
  • API: POST/GET /api/4e/organizations, GET/PUT /api/4e/organizations/{id}, POST /api/4e/organizations/{id}/lifecycle (Admin), GET /api/4e/organizations/{id}/tenants.
  • Files: Common/Organizations/**, Api/Controllers/OrganizationsController.cs. Lifecycle writes a control_plane_event.

Cell management

Plain: register the "wings" where tenants live and toggle whether they accept traffic.

  • CQRS: CreateCellHandler, GetCellHandler, SearchCellsHandler, UpdateCellHandler, CellStatusHandler.
  • API: POST /api/4e/cells (Admin), GET /api/4e/cells, GET /api/4e/cells/{id}, PUT /api/4e/cells/{id} (Admin), POST /api/4e/cells/{id}/status (Admin).
  • Files: Common/Cells/**, Api/Controllers/CellsController.cs. A status change emits cell.status_changed.

Tenant lifecycle & operations

Plain: the big one — create a tenant; search/get it; move it through its lifecycle; move it to a different cell; clone it; delete it; turn its modules on/off; record migration evidence; issue/revoke API keys.

  • CQRS: CreateTenantHandler, SearchTenantsHandler, GetTenantHandler, TenantLifecycleHandler, MoveTenantHandler, CloneTenantHandler, DeleteTenantHandler, EnumerateTenantsHandler (Common/Tenants/**); module handlers ModuleEntitlementLifecycle* + UpdateModuleEntitlementHandler (Common/ModuleEntitlements/**); RecordMigrationRunHandler; IssueTenantApiKeyHandler / RevokeTenantApiKeyHandler / GetTenantApiKeysHandler; SearchTenantOperationsHandler.
  • API (all on TenantsController.cs): POST/GET /api/4e/tenants, GET /api/4e/tenants/{id}, GET .../operations, POST .../impersonation-sessions (Support), PUT .../modules/{moduleCode} (enable), POST .../modules/{moduleCode}/suspend|disable (Manage), .../revoke (Admin), POST .../migration-runs, POST/GET .../api-keys, DELETE .../api-keys/{id} (Admin), POST .../move|clone (Admin), DELETE /api/4e/tenants/{id} (Admin), POST .../lifecycle (Admin), GET .../modules.
  • Flow: module-lifecycle writes publish RabbitMQ events (module.enabled/disabled/suspended/revoked.v1) and invalidate the runtime resolution cache. Enabling a module on an Active tenant auto-enqueues an enable-module-runtime provisioning job. Every lifecycle op appends a tenant_operations row.

Tenant runtime resolution (consumed by the ERP runtime)

Plain: the runtime asks 4E "is this tenant allowed online, and which modules?" — this is the read the whole platform leans on.

  • A tenant is routable only when tenant + org + cell are all Active (TenantRuntimeComposition); enabled modules come only from Enabled entitlements.
  • Services: ResolveTenantRuntime* / EnumerateCellTenantRuntimesAsync on the facade; cache via RedisTenantRuntimeResolutionCache (cache-aside, invalidated after committed entitlement/lifecycle writes).
  • API: GET /api/4e/tenants/runtime, GET /api/4e/cells/{cellId}/tenant-runtimes — policy FourERuntimeService (TenantRuntimeController.cs).

Subscription catalog & pricing

Plain: the price list. Operators build plans, modules, and priced tiers; the public website reads a safe projection of it.

  • CQRS: Common/Plans/** (Create/Get/Search/Update/Retire/Modules), Common/PricingTiers/** (Create/Get/Search/Update), Common/PublicCatalog/Search (SearchPublicPlansHandler).
  • API: POST/GET /api/4e/catalog/plans (Admin/Read), GET/PUT .../{id}, POST .../{id}/retire|modules|pricing-tiers, GET/PUT .../{id}/pricing-tiers[/{id}] (PlansController.cs); public GET /api/public/plans (anonymous + PublicCatalog rate limit, PublicCatalogController.cs).
  • Seeding: SubscriptionPlanSeeder seeds System plans (legacy-core, system-dependency) and backfills module_entitlements.PlanId; run via Host --seed-billing-catalog (dev/ops only, fail-loud on unmapped plan codes).

Self-serve signup → Checkout → paid provisioning

Plain: the first-revenue path — an anonymous visitor becomes a paying, provisioned tenant with no operator in the loop. (Detailed sequence is in How it works.)

  • CQRS/services: PublicSignupCommand + LivePublicSignupService / IPublicSignupService; CreateCheckoutSessionHandler; ProvisionPaidTenantHandler (idempotent on Stripe subscription id); PublicKycApplicationCommand + KYC service.
  • API (anonymous + rate-limited): POST /api/public/signup, GET /api/public/signup/status (PublicSignupController.cs); POST /api/public/kyc/applications (PublicKycController.cs).

Stripe webhook ingress + dispatch

Plain: verify the signature, store the event, then process it asynchronously so the lifecycle keeps in sync (paid → record invoice, failed → suspend, cancelled → revoke entitlements).

  • Services: StripeWebhookSignatureVerifier / IStripeWebhookSignatureVerifier; LiveStripeWebhookIngressService; StripeWebhookEventDispatcher + StripeWebhookDispatchRouting / StripeWebhookEventTypes; handlers RecordInvoiceFromStripeHandler, SyncSubscriptionFromStripeHandler, CancelSubscriptionFromStripeHandler (Common/Checkout/Subscriptions/**).
  • API: POST /api/public/webhooks/stripe (anonymous + StripeWebhook rate limit, StripeWebhookController.cs); legacy operator POST /api/4e/billing/webhooks/{provider}.

Billing reads + operator subscription create

Plain: operators can create a subscription directly and read a tenant's billing snapshot.

  • CQRS: CreateBillingSubscriptionHandler, GetTenantBillingSnapshotHandler, RecordBillingWebhookHandler (Common/Billing/**).
  • API: POST /api/4e/billing/subscriptions (Manage), GET /api/4e/billing/tenants/{tenantId} (Read) (BillingController.cs).

Tenant-user invitations (Keycloak)

Plain: invite the first (and later) users into a tenant, then resend / revoke, and grant the verified-admin role once they confirm their email.

  • CQRS/services: ITenantUserProvisioningService + Common/TenantUsers/** (InviteTenantUserHandler, ResendInvitationHandler, RevokeInvitationHandler, SearchInvitationsHandler, GetInvitationHandler, ProvisionPrimaryAdminHandler, GrantVerifiedAdminRoleHandler, SkipPrimaryAdminHandler).
  • API: POST .../users/invitations (Manage + IdentityInvite rate limit), GET .../invitations[/{id}] (Read), POST .../invitations/{id}/revoke (Manage), .../resend (Manage + rate limit) (TenantUsersController.cs).

Impersonation (signed, read-only)

Plain: a support person gets a time-boxed, read-only token to look inside a tenant. The token is HMAC-signed and the runtime validates it server-side; every attempt is logged.

  • CQRS/services: IImpersonationTokenSigner / ImpersonationTokenSigner; Common/Impersonation/** (IssueImpersonationTokenHandler, RevokeImpersonationGrantHandler, SearchImpersonationGrantsHandler, ValidateImpersonationTokenHandler, CreateImpersonationGrantHandler).
  • API: POST /api/4e/impersonation (Support, returns token once), GET /api/4e/impersonation (Read), DELETE /api/4e/impersonation/{grantId} (Support), POST /api/4e/impersonation/{tokenId}/validate (Read — runtime handshake) (ImpersonationController.cs). Every attempt writes one control_plane_event.

Provisioning jobs (enqueue + evidence)

Plain: the operator API to enqueue a provisioning job and read its progress / evidence.

  • CQRS: CreateProvisioningJobHandler, GetProvisioningJobHandler, GetProvisioningEvidenceHandler, UpdateProvisioningJobHandler (Common/ProvisioningJobs/**).
  • API: POST /api/4e/provisioning-jobs (Manage), GET /api/4e/provisioning-jobs/{id}, GET .../{id}/evidence (Read) (ProvisioningJobsController.cs).

Control-plane monitoring + dead letters

Plain: the logbook and incident desk — record health snapshots and events, roll up fleet health, and triage / retry / resolve failed messages from the dead-letter queue.

  • CQRS: Common/Monitoring/** (CreateHealthSnapshotHandler, SearchHealthSnapshotsHandler, CreateEventHandler, SearchEventsHandler, HealthRollupHandler); Common/DeadLetters/** (SearchDeadLettersHandler, GetDeadLetterHandler, RetryDeadLetterHandler, ResolveDeadLetterHandler).
  • API: POST/GET /api/4e/monitoring/health-snapshots, POST/GET /api/4e/monitoring/events, GET /api/4e/monitoring/rollup, GET /api/4e/monitoring/dead-letters[/{id}], POST .../dead-letters/{id}/retry|resolve (MonitoringController.cs).

Cross-tenant owner identity

Plain: one human can own a portfolio of several tenants. Operators link / claim / unlink / verify those ownership links.

  • CQRS/services: IOwnerPortfolioService + Common/Owners/** (CreateOwnerProfileHandler, UpdateOwnerProfileHandler, SearchOwnersHandler, GetOwnerProfileHandler, LinkCompanyHandler, ClaimCompanyHandler, UnlinkCompanyHandler, VerifyCompanyLinkHandler, GetOwnerCompaniesHandler).
  • API (OwnersController.cs): POST/GET /api/4e/owners, GET/PUT .../{ownerId}, POST .../{ownerId}/companies (link), .../companies/claim, .../companies/{tenantId}/unlink, .../company-links/{memberId}/verify, GET .../{ownerId}/companies.

Executive portal

Plain: the owner-facing side. An owner reads their own portfolio, sees a company directory, and mints a single-use launch token to single-sign-on into one of their tenant ERPs.

  • CQRS/services: IOwnerPortfolioService (ResolvePortalContextHandler, GetCompaniesHandler, GetCompanyHandler, IssueLaunchTokenHandler, UpdateMyProfileHandler, VerifyLaunchTokenHandler); ILaunchTokenIssuer / LaunchTokenIssuer; IRuntimeOwnerAccessProbe (config-gated, fail-loud default).
  • API: GET /api/4e/portal/me, GET .../me/companies[/{tenantId}], POST .../me/companies/{tenantId}/launch-token, PUT .../me — policy ExecutivePortalOwner (OwnerPortalController.cs); GET /api/4e/portal/access-probe, GET /api/4e/portal/company-directory — policy FourEPortalExecutive (PortalController.cs); POST /api/4e/portal/launch-token/verify — policy FourERuntimeService (PortalLaunchVerifyController.cs).

How it talks to other modules

4E is the upstream control plane. It does not consume business events from any ERP runtime module; instead it produces entitlement/routing evidence the runtime consumes, plus it integrates with the external systems Keycloak, Stripe, Redis and RabbitMQ.

Events produced (4E → RabbitMQ). SSOT is FourEControlPlaneEventNames (Common/ProvisioningJobs/Saga/Steps/ControlPlaneMessages.cs). Every message is published under a per-cell key 4e.cell.{cellId}.{moduleCode} and a stable second key 4e.entitlements.v1, so the runtime feed binds one queue without knowing per-cell keys.

EventProduced byConsumer
tenant-runtime-provisioned.v1saga rabbit-event-publish stepruntime entitlement feed
module.enabled.v1entitlement enable handlerruntime entitlement feed
module.disabled.v1disable handlerruntime entitlement feed
module.suspended.v1suspend handlerruntime entitlement feed
module.revoked.v1revoke handler (also Stripe cancel path)runtime entitlement feed
tenant.suspended.v1tenant lifecycle suspendruntime entitlement feed
tenant.resumed.v1tenant lifecycle restoreruntime entitlement feed

Events consumed (RabbitMQ → 4E). Only the control-plane Dead Letter QueueControlPlaneDeadLetterIntakeBackgroundService is the sole RabbitMQ consumer. 4E does not consume business events from any ERP runtime module.

External webhook (HTTP). Stripe → POST /api/public/webhooks/stripe → inbox → DB-poll dispatcher. Handled types: checkout.session.completed, invoice.paid, invoice.payment_failed, customer.subscription.updated / deleted.

Boundaries.

  • → ERP runtime: the runtime calls GET /api/4e/tenants/runtime + GET /api/4e/cells/{id}/tenant-runtimes (FourERuntimeService) and consumes 4e.entitlements.v1; it redeems portal launch tokens via POST /api/4e/portal/launch-token/verify. 4E calls into the runtime only via the config-gated HttpRuntimeOwnerAccessProbe.
  • → Auth: Auth stays the record-rule / company-scope authority; 4E integrates no record-rule evaluator. The session bridge writes OwnerProfile.KeycloakSubjectId into the runtime Auth UserProfile.KeycloakSubject.
  • → Keycloak / Stripe / Redis / RabbitMQ: typed admin clients, config-gated Stripe client, cache-aside resolution cache, and the control-plane publish + DLQ topology, respectively.
  • Shared kernel: uses ERP.SharedKernel.Results, ERP.SharedKernel.Security.RuntimeVerticalTypes, and ERP.Host.Common. It references no other module's Entity project.
  • Separate product: its own DB (four_e_organizations / schema four_e), Redis, RabbitMQ and deploy, fronted (like the runtime) by ERP.ApiGateway. Vertical packs (Alfawood / InsuranceBroker) own no 4E behavior.
As-built gaps & stubs

The current code diverges from the design/BRD in several honest ways. Trust the code over the older docs.

  1. DATABASE.md is stale. It documents only 11 "foundation" tables and an unchecked "remaining work" list; the live schema is 29 tables and most mutable tables now carry an xmin RowVersion. Trust the migrations over docs/applications/4EOrganizationsManager/DATABASE.md. The wiki note docs/wiki/modules/4e-organizations-manager.md is closer but predates the billing/owner/portal expansions.
  2. No RLS anywhere — intentional, but flag it. Unlike every ERP runtime module, 4E has zero Row-Level Security. Isolation rests entirely on the explicit TenantId FK + authorization policies. A handler bug that omits a tenant filter is not caught by a database safety net.
  3. Expand-phase nullable FKs not yet contracted. tenants.vertical_type and module_entitlements.PlanId are still nullable, with the NOT NULL step explicitly deferred. Until backfill is verified in every environment, a tenant/entitlement can exist without a vertical / plan link.
  4. Executive portal probe is partly a placeholder. GET /api/4e/portal/access-probe proves auth-scheme separation only; it does not complete the later L4 executive-portal feature set. The richer OwnerPortalController endpoints are real, but the runtime owner-access probe is config-gated and fail-loud-stubbed (FailLoudRuntimeOwnerAccessProbe → HTTP 503) until FourE:RuntimeOwnerAccessProbe:BaseAddress is configured.
  5. Stripe billing is config-gated and ships unconfigured. Without FourE:StripePlatformBilling:ApiKey + Checkout URLs, the FailClosedStripeBillingClient stays bound (fail loud). The live catalog today is effectively the two seeded System plans with ~zero real invoices, so billing-money read paths are correct but exercised against near-empty data.
  6. Billing webhook money / email correctness — recently fixed, was real. docs/quality/erp-next/audit/findings-x-4e.md records four genuine defects, all RESOLVED 2026-06-14: (a) a clean Stripe cancel left entitlements Enabled (revenue leakage) → now routes through the shared RevokeModuleEntitlementCommand; (b) the dispatcher swallowed non-retryable failures as Processed, silently dropping paid-tenant provisioning → now fail-loud with DispatchAttemptCount + a DeadLettered cutoff (migration AddBillingWebhookDispatchAttemptCount); (c) invoice money fields persisted as 0 → now read from the payload; (d) the founder-email guard was bypassed when customer_email was null → now reads nested customer_details.email.
  7. Legacy/duplicate impersonation + webhook paths coexist. The signed-grant impersonation path (/api/4e/impersonation) is additive and separate from the legacy support-session path (POST /api/4e/tenants/{id}/impersonation-sessions). Likewise POST /api/4e/billing/webhooks/{provider} coexists with the live POST /api/public/webhooks/stripe. Two flows per concept — consolidation candidates, currently kept for migration stability.
  8. FourEOrganizationsAdmin legacy policy alias retained (with the migration-only erp_4e_admin=true claim) even though no current controller references it directly; graded operator policies are used instead. Kept for backward compatibility.
  9. Migration evidence has no RowVersion. migration_runs and the single-use owner_company_launch_tokens deliberately omit xmin; they rely on unique-key idempotency / an atomic null-predicate update instead of optimistic concurrency. Correct for append/single-use rows, but inconsistent with the rest of the model — note this when reasoning about concurrency.