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

Tenant and Onboarding

Every data plane the EDK ships is multi-tenant. The issuer, verifier, AS, DID resolver, and KMS containers all run as single processes that serve any number of tenants. Tenant is the unit of routing, the unit of authorization, the unit of persistence isolation, and the unit of configuration. A set of EDK tenant modules and the services-tenant-rest admin surface is what makes that work.

This section covers the parts a deployment actually touches: the tenant model itself, the registration journeys an operator or prospective tenant goes through to land in the system, the resolver chain that maps an incoming HTTP request to a tenant, the public-endpoint binding the data planes consult when they advertise URLs, the per-tenant configuration storage, and the application tenant that owns the control plane.

EDK tenant module stack

The Module Stack

The tenant subsystem is published as a set of Maven artifacts under the com.sphereon.edk group. The stack is:

  • lib-tenant-public carries the models, inputs, and SPIs: Tenant, TenantStatus, TenantDomain, TenantDomainKind, TenantPublicEndpoint, TenantPublicEndpointServiceType, TenantSlug, TenantRoles, TenantBootstrapStatus, TenantSignupRequest, TenantSignupStatus, TenantRegistrationStep, TenantOnboardingStatus, License, LicenseLimits, LicenseFeatures, RegisterTenantArgs, OwnerInput (sealed: Local, Federated, Hybrid), OnboardingChannel, TenantResolutionSettings, TenantConfigSecretClassifier, HttpHostTenantInput, and the TenantInvalidationBroker interface.
  • lib-tenant-service carries the service commands and the policy SPIs: RegisterTenantServiceCommand, ReconcileTenantRegistrationServiceCommand, BootstrapTenantServiceCommand, GetTenantOnboardingStatusServiceCommand, TenantCrudServiceCommands, TenantDomainServiceCommands, TenantPublicEndpointServiceCommands, AsInstanceAdminCommands, the signup command set, the onboarding-policy SPI (TenantOnboardingPolicy with FailClosedTenantOnboardingPolicy as the default), the signup-policy SPIs (TenantSignupPolicy, SignupChallengeVerifier, SignupAuthorizationPolicy, SignupTokenHasher), the license and quota services (LicenseService, TenantQuotaService, TenantServiceInstancePolicy), the audit emitter, the schema initializer, the per-tenant isolation strategy selector, and the resolution settings reader.
  • lib-tenant-persistence-api and lib-tenant-persistence-postgresql carry the repositories. The Postgres impl is the production binding: TenantRoutingRepository, TenantRoutingResolver, TenantDomainRepository, TenantPublicEndpointRepository, TenantConfigPropertyRepository, TenantBootstrapRepository, TenantSignupRequestRepository, TenantRegistrationLogRepository, plus the TenantRegistryDatabase SQLDelight schema (tenant_routing, tenant_domain, tenant_public_endpoint, tenant_config_property, tenant_bootstrap, tenant_signup_request, tenant_registration_log, tenant_registration_step_log).
  • lib-tenant-provisioning carries the TenantProvisioner and the JDBC maintenance executor that creates per-tenant schemas, databases, or hosts when a tenant is provisioned with non-shared isolation.
  • lib-tenant-resolution-impl carries the resolver chain: PlatformSubdomainTenantResolver, CustomDomainTenantResolver, EdkRoutableSlugLookup, the TenantResolutionSettingsBinder, and the in-memory resolver cache.
  • lib-tenant-config-source carries the TenantConfigPropertySource and its source-contribution wiring; together with the IDK property resolver chain this gives the runtime the App, Tenant, and Principal config scopes.
  • lib-tenant-federation/{public,service} carries the per-tenant IDP registry SPIs (TenantIdpRepository, TenantIdpSecretStore, TenantIdpConnectivityProbe) and their default impls.
  • services-tenant-rest mounts the TenantAdminHttpAdapter at /api/v1/tenants with TenantPathPolicy.None so the path's {tenantId} is the operation target rather than the acting tenant. The acting tenant always comes from the JWT.
  • services-application-rest mounts the ApplicationAdminHttpAdapter at /api/v1/application for control-plane operations (application tenant bootstrap, license upload and verification, secret backend selection, onboarding policy).

The Tenant Model

The EDK Tenant is a superset of the IDK projection at com.sphereon.data.store.party.model.Tenant. The IDK projection stays smaller so the open-core SDK exposes the minimal surface mobile and standalone consumers need; the server EDK uses the richer model.

The fields hierarchical multi-tenant routing requires, beyond what the IDK projection has:

  • slug is the globally unique URL-safe label that doubles as the platform subdomain label and as the peelable path segment in the resolver dispatcher. Lowercase, hyphen-separated, must match ^[a-z][a-z0-9-]{0,62}$, no consecutive hyphens.
  • parentTenantId is the self-reference that gives the parent/child hierarchy. null for root tenants. Cycle detection runs at insert time by walking up parent_tenant_id.
  • status is the explicit lifecycle marker: ACTIVE (fully provisioned and reachable), SUSPENDED (registry rows present but the resolver rejects requests with tenant_suspended), or PENDING_VERIFICATION (registered but a required step is outstanding, such as owner email verification or a custom-domain DNS challenge).
  • system is a generic flag for "this is not a customer tenant". System tenants are excluded from default listings and from slug-based and parent-based resolution. Higher layers (VDX) use it to materialise their control-plane tenant; pure-EDK consumers can use it for their own system-tenant patterns. The EDK itself does not assign privileges to the flag.
  • tenantType mirrors the IDK projection (ORGANIZATION, INDIVIDUAL, and so on).

A tenant has zero or more TenantDomain rows. Two kinds:

  • PLATFORM_SUBDOMAIN. The SaaS-owned host of the form <slug>.<platform-base>. Auto-created at tenant registration. Always verified at insert time, because the platform controls the DNS for <platform-base>.
  • CUSTOM_DOMAIN. A host the customer brings (wallet.acme.com). Inserted unverified. The resolver chain skips unverified custom-domain rows, so the customer can preserve them in the registry while completing the DNS verification flow.

A tenant has zero or more TenantPublicEndpoint rows. Each row binds a (tenant, serviceType) to the host, pathPrefix, and wellKnownPath under which that service is reachable for that tenant. The service types are OID4VCI_ISSUER, OID4VP_VERIFIER, and OAUTH2_AUTHORIZATION_SERVER. The data planes consult these rows when they advertise URLs in metadata, in credential_offer_uri, in OID4VP request_uri_base, in OAuth issuer claims, and so on.

Registration Goes Through One Place

RegisterTenantServiceCommand is the single command every tenant registration goes through. Whether the registration arrives via the admin REST, via an admin invite, via self-service signup, or via the one-shot BootstrapTenantServiceCommand, the work to actually create the tenant lives in the same command implementation. That command is responsible for the full atomic bootstrap:

  1. Authorising the call through the TenantOnboardingPolicy (the first thing it does, before any DB query, so an unauthorised caller cannot enumerate slug uniqueness through a registration probe).
  2. Validating slug, parent existence, cycle absence, and owner shape.
  3. Inserting the tenant_routing sidecar row and the initial platform subdomain (ROUTING_INSERTED).
  4. Provisioning the per-tenant storage container per the configured isolation strategy (ISOLATION_PROVISIONED).
  5. Ensuring the per-tenant domain schemas (TENANT_SCHEMAS_ENSURED) and the user/group/role schema (USER_SCHEMA_ENSURED).
  6. Provisioning the default Authorisation Server instance when hostedAs = true (AS_PROVISIONED).
  7. Creating the owner User row and minting the owner invitation token for local owners (OWNER_PROVISIONED, OWNER_INVITATION_MINTED).

Each step is recorded into tenant_registration_log as a TenantRegistrationStepRecord. The reconcile janitor (ReconcileTenantRegistrationServiceCommand) walks the STANDARD_COMPENSATION_ORDER in reverse on failure and calls the matching deprovision delegate for each step that completed. Higher layers extend compensation by recording their own step ids and supplying their own reverse logic.

The shape of "where did this call come from" is carried out-of-band on a session-scoped OnboardingChannelHolder. The calling adapter (REST endpoint, signup-confirm command, bootstrap command) populates the holder with one of three OnboardingChannel variants before delegating:

  • Operator for an authenticated admin call. Carries the session tenant id and the roles extracted from the validated JWT.
  • SelfServiceSignup for the materialisation of a confirmed signup request. Carries only the signup request id; the policy loads the row and derives parent, slug, and email from there.
  • Bootstrap for the one-shot first-tenant claim. Only BootstrapTenantServiceCommand produces this channel, and only after it has atomically claimed the durable tenant_bootstrap gate.

The fail-closed EDK default (FailClosedTenantOnboardingPolicy) rejects every Operator and SelfServiceSignup channel. Production deployments bind a real TenantOnboardingPolicy that interprets roles and signup state. A pure-EDK build that forgets to bind a real policy fails closed rather than fails open.

Four Registration Journeys

Four distinct user-facing journeys lead into RegisterTenantServiceCommand. Each is documented in detail on its own page; the high-level shape is:

  • Admin direct creation. An authenticated admin posts to /api/v1/tenants. The tenant is created immediately. The only journey that works when no SMTP / email service is configured. See Registration Journeys.
  • Admin invite by email. Same shape as admin direct creation, except after the tenant is created the EDK hands the owner invitation token to the enterprise-email-service server-side and the owner receives an activation email. Requires SMTP. See Registration Journeys.
  • Self-service signup. A prospective tenant owner posts to /api/v1/tenants/signup/request. The flow walks through PENDING_EMAIL, PENDING_APPROVAL (when policy requires operator approval), CONFIRMED, and finally REGISTERED. Requires SMTP and is gated by the self-signup license feature. See Registration Journeys.
  • Bootstrap. The one-shot first-tenant claim through BootstrapTenantServiceCommand. Used to bring up the very first real tenant on a fresh deployment. See Application Tenant and Bootstrap.

License gates apply across all four journeys: maxRootTenants, maxTotalTenants, maxHierarchyDepth, subtenantsAllowed, and the subtenants / self-signup / custom-domains / federation feature flags. Pure-EDK builds bind UnboundedLicenseService so registration is unconstrained unless a real license-service impl replaces it.

What This Section Covers

  • Tenant Model. The Tenant shape, status lifecycle, hierarchy, slugs, system tenants, and how the EDK Tenant relates to the IDK projection.
  • Tenant Resolution. The layered resolver chain (JWT, subdomain, custom domain, path slug), the well-known URL forms, the in-memory cache, and the cross-replica invalidation channel.
  • Domains and Public Endpoints. TenantDomain (platform subdomain and verified custom domain), TenantPublicEndpoint (per-service URL binding), and how the data planes consult them.
  • Registration Journeys. The four ways a tenant lands in the system: admin direct, admin invite, self-service signup, and bootstrap. End-to-end flows, web screens, license gates, and the signup state machine.
  • Application Tenant and Bootstrap. The control-plane tenant, the durable bootstrap gate, the application admin REST under /api/v1/application, license activation and snapshot loading.
  • License, Quota, and Policy. License, LicenseLimits, LicenseFeatures, the onboarding-policy SPI, the signup-policy SPIs, and how a deployment overlays its own.
  • Per-Tenant Configuration. The tenant_config_property table, the TenantConfigSecretClassifier, secret references through the selected backend, and the App / Tenant / Principal scope chain.
  • Tenant Isolation. Row-level vs per-tenant database isolation, the TenantProvisioner, per-tenant signing keys, encryption at rest, and how a tenant admin's reach is constrained.

What This Section Does Not Cover

  • Multi-instance fan-out per tenant (N issuer instances per tenant for N credential programs). This is a VDX capability built on the party model.
  • Operator dashboards and UIs. The EDK exposes the REST surface; the UI is a layer above.
  • Cross-tenant catalog projection. VDX.
  • Durable workflow execution beyond the EDK's reconcile janitor. The janitor handles registration step compensation; longer-running sagas live in VDX.
  • License-format details (signed JSON, JWT). EDK only exposes the parsed snapshot; concrete formats live in VDX.