Tenant Model
Tenant is the central entity in the EDK control plane. Every other entity in the EDK that is not deployment-wide (configuration rows, integrations, credential designs, DCQL queries, AS clients, federation providers, signing key aliases, webhook configurations, sessions, audit events) carries a tenant_id. The shape of the tenant is therefore the first thing to nail down.
EDK Tenant vs IDK Tenant
The IDK ships a smaller projection at com.sphereon.data.store.party.model.Tenant. That projection is the surface the open-core SDK exposes to mobile and standalone consumers; it carries only the minimum fields the SDK needs to identify a tenant and its parent. Standalone EDK consumers (a wallet app embedding EDK libraries) continue to work against the IDK Tenant unchanged.
The server EDK extends that projection with the fields a real multi-tenant deployment needs. The server entity is com.sphereon.edk.tenant.model.Tenant, and the additional fields it carries beyond the IDK projection are:
slugis a globally unique URL-safe label, lowercase and hyphen-separated, matching^[a-z][a-z0-9-]{0,62}$with no consecutive hyphens. The slug doubles as the platform subdomain label and the peelable path segment in the resolver dispatcher.parentTenantIdgives the parent for hierarchical tenancy.nullfor root tenants. Cycle detection runs at insert time by walking upparent_tenant_id.statusis the explicit lifecycle marker. The IDK projection only models soft delete; the EDK addsACTIVE,SUSPENDED, andPENDING_VERIFICATION.systemflags a tenant as a non-customer / internal tenant. System tenants are excluded from default listings and from slug-based and parent-based resolution queries.- The standard EDK audit fields:
createdById,updatedById,deletedByIdalongside the IDK'screatedAt,updatedAt,deletedAt.
The server EDK code imports the richer Tenant directly; the IDK Tenant is reserved for the open-core SDK boundary.
Slugs
The slug carries a lot of weight. It is:
- The platform subdomain label.
acmebecomesacme.<platform-base>and resolves to the tenant. - The peelable path segment for path-slug routing.
/acme/oid4vci/...resolves to theacmetenant for the matching adapters. - A path segment in the
.well-knownURL forms for OID4VCI (/.well-known/openid-credential-issuer/acme), OAuth AS metadata (/.well-known/oauth-authorization-server/acme), and OIDC Discovery (/acme/.well-known/openid-configuration).
Globally unique slugs mean any tenant can be reached by direct subdomain regardless of where it sits in the hierarchy. tenantc.saas.com resolves to tenantc even when tenantc is a child of tenanta. Service-specific labels such as issuer, verifier, or auth may sit to the left of the tenant slug without changing the resolution result: issuer.tenantc.saas.com still resolves to tenantc.
Validation rules:
- Length: between 1 and 63 characters (the DNS label limit).
- Character set:
[a-z0-9-], must start with[a-z]. - No consecutive hyphens.
- A small reserved-word denylist (
admin,api,www,system, plus deployment-specific entries the operator can extend).
Slug uniqueness is enforced at the database level on the tenant_routing sidecar table. The tenant admin API rejects collisions with a 409 before attempting registration; the unique index is the backstop.
Hierarchy
Parent / child hierarchy is a self-reference: parentTenantId points at another tenant, or is null for roots. The EDK uses the hierarchy for two things:
- Authorisation scope. A parent tenant admin sees and manages its child tenants; a child tenant admin sees only its own resources.
- License limits.
maxHierarchyDepthcaps how deep the tree can go (nullparent = depth 1, one child = depth 2, and so on).
Hierarchy is not used for tenant data inheritance. A child tenant's configuration, integrations, signing keys, designs, queries, and sessions are independent of the parent's. The hierarchy exists for organisational and licensing reasons, not for data sharing.
Cycle detection runs at insert time. The RegisterTenantServiceCommand walks up the proposed parent's ancestry; if the path is longer than maxHierarchyDepth or rejoins the registering tenant's id, registration fails before any side effect is recorded.
Status Lifecycle
ACTIVE. Fully provisioned. The resolver returns the tenant; admin and protocol surfaces work.SUSPENDED. The registry rows are still present (no data loss), but the resolver returns atenant_suspendederror rather than the tenant id. Used to disable a tenant without deleting its data, for example during a billing dispute, a security incident, or a manual operator hold.PENDING_VERIFICATION. The tenant has been registered but a required verification step (owner email verification, custom-domain DNS challenge, or another deployment-specific gate) is still outstanding. The resolver returns the tenant so the verification flow can run, but admin commands gated by verification refuse to run.
Status transitions go through UpdateTenantStatusHttpEndpointCommand (the /api/v1/tenants/{id}/status endpoint). The transition emits a TenantInvalidationBroker event so every replica's resolver cache picks up the change without a restart.
Soft delete is orthogonal to status: deletedAt non-null removes the tenant from listings and from resolution, and a tenant in any status can be soft-deleted. Hard delete is not a standard operation; the audit trail and the foreign-key chains across business tables make a hard delete a multi-table cascade that is intentionally not exposed at the REST surface.
The System Flag
The system boolean is a generic marker for "this tenant is not a customer". System tenants:
- Are excluded from
ListTenantsHttpEndpointCommandby default. Operators that want to see them pass an explicitincludeSystem=trueflag. - Are not findable by slug-based or parent-based queries through the standard resolver. The resolver only returns tenants with
system = false. - Count differently for quota purposes.
maxRootTenantsandmaxTotalTenantsare limits on customer tenants only.
The EDK itself does not define what a system tenant is for or what privileges it carries. Higher layers use the flag to materialise their control-plane patterns:
- VDX materialises its admin-control tenant as a system tenant.
- The EDK application tenant (the
enterprise-as-hosted control plane that owns license activation and onboarding setup) is a system tenant on EDK-only deployments. - Pure-EDK consumers can use the flag for their own system-tenant needs.
The system flag is settable only at registration. There is no API to flip a customer tenant into a system tenant or vice versa after creation; the constraint avoids tenant counts changing meaning under the operator's feet.
Audit Fields
Every tenant carries:
createdAt,createdByIdupdatedAt,updatedByIddeletedAt,deletedById
*ById is a Uuid? keyed to the principal who performed the operation, derived from the calling JWT. For self-service signup registrations, the field is populated with the synthetic signup principal id rather than the would-be tenant owner's id (the owner doesn't exist as a principal at registration time).
The audit fields are mirrored into the audit stream as well: every admin command emits a structured audit event with the tenant id, the principal, the command id, the result, and the relevant business identifiers. The TenantAuditEmitter is the SPI that contributes the tenant-specific event payloads.
Where the Model Lives
| Concern | Module |
|---|---|
Tenant, TenantStatus, TenantDomain, TenantPublicEndpoint, TenantSlug, RegisterTenantArgs, OwnerInput | com.sphereon.edk:lib-tenant-public |
TenantBootstrapStatus, TenantSignupRequest, TenantSignupStatus, OnboardingChannel | com.sphereon.edk:lib-tenant-public (under com.sphereon.tenant.*) |
TenantOnboardingStatus, TenantRegistrationStep, TenantRegistrationStepRecord, TenantRegistrationLog | com.sphereon.edk:lib-tenant-public |
RegisterTenantServiceCommand, TenantCrudServiceCommands, TenantDomainServiceCommands, TenantPublicEndpointServiceCommands, signup commands | com.sphereon.edk:lib-tenant-service |
| Postgres repositories and SQLDelight schemas | com.sphereon.edk:lib-tenant-persistence-postgresql |
TenantProvisioner, JdbcMaintenanceJdbcExecutor | com.sphereon.edk:lib-tenant-provisioning |
Resolver chain (PlatformSubdomainTenantResolver, CustomDomainTenantResolver, EdkRoutableSlugLookup, cache) | com.sphereon.edk:lib-tenant-resolution-impl |
TenantConfigPropertySource, source-contribution wiring | com.sphereon.edk:lib-tenant-config-source |
| Per-tenant IDP federation SPIs and default impls | com.sphereon.edk:lib-tenant-federation-public / -service |
TenantAdminHttpAdapter mounted at /api/v1/tenants | com.sphereon.edk:services-tenant-rest |
ApplicationAdminHttpAdapter mounted at /api/v1/application | com.sphereon.edk:services-application-rest |