Skip to main content
Version: v0.25.0 (Latest)

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:

  • slug is 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.
  • parentTenantId gives the parent for hierarchical tenancy. null for root tenants. Cycle detection runs at insert time by walking up parent_tenant_id.
  • status is the explicit lifecycle marker. The IDK projection only models soft delete; the EDK adds ACTIVE, SUSPENDED, and PENDING_VERIFICATION.
  • system flags 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, deletedById alongside the IDK's createdAt, 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. acme becomes acme.<platform-base> and resolves to the tenant.
  • The peelable path segment for path-slug routing. /acme/oid4vci/... resolves to the acme tenant for the matching adapters.
  • A path segment in the .well-known URL 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. maxHierarchyDepth caps how deep the tree can go (null parent = 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

Tenant 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 a tenant_suspended error 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 ListTenantsHttpEndpointCommand by default. Operators that want to see them pass an explicit includeSystem=true flag.
  • 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. maxRootTenants and maxTotalTenants are 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, createdById
  • updatedAt, updatedById
  • deletedAt, 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

ConcernModule
Tenant, TenantStatus, TenantDomain, TenantPublicEndpoint, TenantSlug, RegisterTenantArgs, OwnerInputcom.sphereon.edk:lib-tenant-public
TenantBootstrapStatus, TenantSignupRequest, TenantSignupStatus, OnboardingChannelcom.sphereon.edk:lib-tenant-public (under com.sphereon.tenant.*)
TenantOnboardingStatus, TenantRegistrationStep, TenantRegistrationStepRecord, TenantRegistrationLogcom.sphereon.edk:lib-tenant-public
RegisterTenantServiceCommand, TenantCrudServiceCommands, TenantDomainServiceCommands, TenantPublicEndpointServiceCommands, signup commandscom.sphereon.edk:lib-tenant-service
Postgres repositories and SQLDelight schemascom.sphereon.edk:lib-tenant-persistence-postgresql
TenantProvisioner, JdbcMaintenanceJdbcExecutorcom.sphereon.edk:lib-tenant-provisioning
Resolver chain (PlatformSubdomainTenantResolver, CustomDomainTenantResolver, EdkRoutableSlugLookup, cache)com.sphereon.edk:lib-tenant-resolution-impl
TenantConfigPropertySource, source-contribution wiringcom.sphereon.edk:lib-tenant-config-source
Per-tenant IDP federation SPIs and default implscom.sphereon.edk:lib-tenant-federation-public / -service
TenantAdminHttpAdapter mounted at /api/v1/tenantscom.sphereon.edk:services-tenant-rest
ApplicationAdminHttpAdapter mounted at /api/v1/applicationcom.sphereon.edk:services-application-rest