Registration Journeys
Tenant registration always converges on the same RegisterTenantServiceCommand. What differs across journeys is how the caller proves they are allowed to register, what the registration's owner-bootstrap path looks like, and which application-level prerequisites must be in place. The four journeys are admin direct creation, admin invite by email, public self-service signup, and the one-shot bootstrap that bring up the first real tenant on a fresh deployment.
The contract every journey honours is the same:
- Tenant creation does not collect tenant IdP client credentials. A tenant configures its own IdP after the tenant owner is active.
- Secrets are never embedded inline in registration payloads. SMTP credentials, IdP client secrets, webhook secrets, and integration secrets always go through the selected secret backend as references.
- License gates run before signup policy. Policy can turn off or constrain a licensed route, but it cannot enable a route the license disallows.
- Email delivery, when enabled, is server-side through the
enterprise-email-servicecontainer. The browser never receives raw signup or invitation tokens.
Prerequisites at the Application Level
Before any production root tenant onboarding path is offered, the deployment must be configured with:
- License activation. A license is installed, its signature and validity window are checked, and the feature / limit snapshot is loaded. Root tenant creation checks
maxRootTenantsandmaxTotalTenants. Self-signup and parent-child availability are derived fromfeatures(self-signup,subtenants) andlimits(subtenantsAllowed). - Optional email service. When
emailTransport = smtp, the deployment runs theenterprise-email-servicecontainer (imagesphereon/enterprise-email-service). The service owns SMTP transport configuration, template rendering, send, test-send, and delivery status / audit. Tenant onboarding calls it server-side. Without SMTP, admin direct creation remains available but invite and self-service signup do not. - Secret management. One active backend is selected for the deployment. Tenant config rows store secret references, not secret values.
- Public URL settings. Application base URL for activation and signup verification links; tenant public-endpoint base host settings.
- Signup policy. Whether root self-service signup is enabled; whether it requires platform approval; rate limits; verification-token TTL.
- Application tenant. Exists before the first real tenant is registered. Has a hosted AS, always. Owns the operator access path used to configure license, email, secret backend, signup policy, and first-tenant onboarding. Stays accessible independently of any real tenant's later external IdP configuration.
See Application Tenant and Bootstrap for the bootstrap sequence and License, Quota, and Policy for the gate behaviour.
Admin Direct Creation
An authenticated administrator posts to /api/v1/tenants and the tenant is created immediately, without an email roundtrip. This is the only journey that works when no SMTP / email service is configured, and it is the safe-mode operator path during initial onboarding or after an outage.
License gates:
- Root creation requires an active license plus available root and total tenant quota.
- Subtenant creation additionally requires the
subtenantsfeature,subtenantsAllowed = true, available total tenant quota, and hierarchy depth withinmaxHierarchyDepth.
Root-level payload:
{
"tenantType": "ORGANIZATION",
"name": "Acme Corp",
"slug": "acme",
"parentTenantId": null,
"initialPlatformSubdomain": true,
"owner": {
"type": "local",
"email": "admin@acme.example",
"displayName": "Acme Admin"
}
}
Subtenant: same shape with parentTenantId set to the parent's id and the slug scoped ("slug": "acme-nl", "parentTenantId": "tenant-acme").
API sequence:
- Admin submits
POST /api/v1/tenantswith a bearer JWT carrying platform-admin scope. RegisterTenantHttpEndpointCommandpopulates theOnboardingChannelHolderwith anOperatorchannel (session tenant id + roles) and delegates toRegisterTenantServiceCommand.- The onboarding policy asserts the call is allowed.
- The command runs the full atomic bootstrap:
tenant_routing, isolation provisioning, schema ensure, default AS (whenhostedAs = true), owner User, owner invitation token. - When the deployment has email configured and the admin flow requests it, the EDK hands the invitation token to the
enterprise-email-serviceserver-side. Otherwise the admin completes owner / tenant onboarding through authenticated admin flows. - The API returns
201 RegisterTenantResponsewith the tenant id, the slug, the primary domain, the issuer URL when available, and the registration correlation id (onceRegisterTenantResultexposes it).
The owner shape is OwnerInput.Local for the standard direct-creation flow. OwnerInput.Federated and OwnerInput.Hybrid are available when the tenant is going straight to an external IdP and the administrator already has the IdP subject identifier on hand.
Important: the registration call does not accept federation provider configuration, OIDC issuer URLs, or OAuth client secrets. Those belong to the post-activation tenant IdP setup that the tenant owner performs through their own admin REST.
Admin Invite By Email
An administrator creates the tenant exactly as in admin direct creation, and the EDK hands the owner invitation token to the enterprise-email-service server-side so the owner receives an activation email. This is the default for local-owner onboarding when SMTP is configured.
Availability: only when the application Email Service is deployed, SMTP transport is configured inside the Email Service, and template / test-send verification has passed.
License gates: same as admin direct creation.
The payload is identical to admin direct creation. The difference is in the post-create step:
- Admin submits
POST /api/v1/tenants. - The command creates the tenant and owner bootstrap state.
- The EDK receives or mints the owner invitation token server-side.
- The EDK calls the configured
enterprise-email-servicewith the owner-invitation template and server-side token context. - The Email Service renders the template and sends the activation email via the configured SMTP transport.
- The API and the admin UI report invite delivery status.
- The owner opens the activation link and completes owner activation through the AS.
The token never leaves the server side. Browser-visible API responses redact the token. The admin UI shows "invite sent", not "here is the link, please email it".
Email Service contract:
template: owner-invitation
serviceAppId: enterprise-email-service
recipient: owner.email
tokenSource: server-side-owner-bootstrap
tokenExposure: server-side-only
transport: lib-email-smtp:SmtpEmailTransport
secretUse:
smtpPassword: secret-reference
Self-Service Signup
A prospective tenant owner starts onboarding from a public signup page or API client. This is the public-facing journey; the EDK validates the request, mints a verification token, and hands it to the Email Service for delivery. The user clicks the link, the EDK confirms the email-token possession, and the tenant is registered (or queued for operator approval, depending on policy).
Availability: SMTP + Email Service required because email-token delivery is required.
License gates:
- Root self-service signup requires an active license, the
self-signupfeature, and available root / total tenant quota. - Subtenant self-service signup additionally requires the
subtenantsfeature,subtenantsAllowed = true, available total tenant quota, and hierarchy depth withinmaxHierarchyDepth.
Root-level payload:
{
"email": "admin@acme.example",
"slug": "acme",
"parentTenantId": null,
"displayName": "Acme Admin",
"challengeProof": "opaque-proof"
}
API sequence:
- Public client submits
POST /api/v1/tenants/signup/request. - Backend validates signup policy, the bot challenge (
SignupChallengeVerifier), slug constraints, and rate limits. - Backend creates a
PENDING_EMAILsignup request, computes the verification token hash viaSignupTokenHasher(SHA-256 by default), and persists the row. - Backend hands the plaintext verification token to the configured
enterprise-email-serviceserver-side. - Email Service renders the template and sends the verification email via SMTP.
- The REST response returns
202 TenantSignupRequestAcceptedwithout the plaintext token. - The owner opens the verification link and submits
POST /api/v1/tenants/signup/confirmwith the token. - If approval is not required, the backend registers the tenant and the row transitions to
REGISTERED. - If approval is required, the row transitions to
PENDING_APPROVAL. - An operator approves or rejects through
POST /api/v1/tenants/signup/{id}/approveor/reject. - Approval registers the tenant and the row transitions to
REGISTERED.
Signup State Machine
State definitions:
PENDING_EMAIL. Request created; verification token issued; waiting for the owner to click the email link.PENDING_APPROVAL. Email verified, but signup policy requires operator approval before registration runs.CONFIRMED. Verification and (optional) approval cleared; tenant registration is allowed to proceed. The row stays inCONFIRMEDwhileRegisterTenantServiceCommandruns.REGISTERED. Registration succeeded;registeredTenantIdis populated.REJECTED. Operator declined before confirmation.EXPIRED. The reconcile janitor closed the row pastexpires_atwithout confirmation.FAILED. Registration was attempted but failed;failureReasoncarries the operator diagnostic.
Signup Policy Keys
tenant.signup.platform.enabled
tenant.signup.platform.requires-approval
tenant.signup.platform.rate-limit.per-email-per-hour
tenant.signup.platform.rate-limit.per-ip-per-hour
tenant.signup.platform.token-ttl-minutes
tenant.signup.children.enabled
tenant.signup.children.requires-approval
tenant.signup.children.rate-limit.per-email-per-hour
tenant.signup.children.rate-limit.per-ip-per-hour
tenant.signup.children.token-ttl-minutes
License is evaluated before policy. Policy can disable or constrain a licensed route, but it cannot enable a route the license disallows.
Resend, Expiry, and Reconciliation
A signup that expires before the user clicks the link can be resent through POST /api/v1/tenants/signup/{id}/resend. The resend service command mints a new verification token under a single atomic UPDATE alongside the new expiresAt, the lastResendAt timestamp, and the incremented resendCount. The per-row min-interval and the max resend count are enforced inside the database, so two replicas racing for the same row cannot bypass the limit.
POST /api/v1/tenants/signup/reconcile/expired runs the reconcile pass that transitions stale PENDING_EMAIL rows past expires_at to EXPIRED. The pass also picks up rows that started registration but never completed and moves them through FAILED when the registration log shows the attempt is stale.
Bootstrap
BootstrapTenantServiceCommand is the one-shot first-tenant claim. Used to bring up the very first real tenant on a fresh deployment, after the application tenant exists and the license is activated.
Mechanism: a single row in tenant_bootstrap carries the durable gate. The row exists with completed_at = null (gate open) on a fresh deployment. BootstrapTenantServiceCommand atomically updates the row to (completed_at = now(), completed_tenant_id, completed_by) under a Postgres SELECT ... FOR UPDATE and a uniqueness constraint that prevents two concurrent calls from both winning. The losing call sees a 409.
Once claimed, the gate is closed for good. Re-opening requires an explicit operator SQL action (UPDATE tenant_bootstrap SET completed_at = NULL), which is intentionally not exposed at the REST surface.
The bootstrap channel (OnboardingChannel.Bootstrap) is the only channel FailClosedTenantOnboardingPolicy accepts unconditionally. The reason: the durable gate IS the gate. By the time the registration command sees OnboardingChannel.Bootstrap, the bootstrap command has already proved the deployment is in its "no real tenants yet" state. There is nothing for the role-based policy to add.
The bootstrap call is invoked through POST /api/v1/application/tenant/bootstrap (on the application admin surface) rather than through the standard /api/v1/tenants mount, because the caller is acting as the application operator rather than as a customer tenant administrator.
After bootstrap, subsequent tenants register through one of the other three journeys.
Operator Approval Queue
For self-service signup journeys that require operator approval, the queue endpoints are:
GET /api/v1/tenants/signup/pending # root signups
GET /api/v1/tenants/signup/pending?parentTenantId=acme # subtenant signups under acme
POST /api/v1/tenants/signup/{signupRequestId}/approve
POST /api/v1/tenants/signup/{signupRequestId}/reject
Platform administrators see root-level pending rows. Parent tenant administrators see child signup rows scoped to their tenant. Each row carries email, slug, displayName, parentTenantId, status, createdAt, expiresAt, resendCount, and the failure / rejection diagnostics where applicable.
Approve immediately attempts registration: the signup row transitions to CONFIRMED, the EDK populates the OnboardingChannelHolder with OnboardingChannel.SelfServiceSignup(signupRequestId), and RegisterTenantServiceCommand runs the bootstrap. Reject records a reason and transitions to REJECTED.
When SMTP Is Absent
A deployment can run without an Email Service, but the operating mode changes:
- Admin direct creation: works. The admin completes owner / tenant onboarding through authenticated admin flows.
- Admin invite by email: unavailable. The admin UI hides or disables the invite flow.
- Self-service signup: unavailable. The public signup endpoints reject with a 503 and the signup screens are hidden.
- Bootstrap: works. Bootstrap does not depend on email.
A typical minimal on-prem or early-bootstrap deployment runs in this mode initially, then enables the Email Service once the platform team is ready to operate the signup queues.