CodamAIDocs
Topicdone

Create and provision a tenant

Store the intent, create and migrate the database, store the result. What happens in SINGLE, in MULTI and with a standalone CIAS.

Variants
MULTI: create the databaseSINGLE: nothingstandalone CIAS: nothingfailure → FAILED, can be retriedstuck rollout → FAILED after 30 min

What this is about

Creating a tenant means two things: writing a record in CIAS and setting up the tenant’s database. One lives in the CIAS database, the other in the CDMS persistence. There is no shared transaction across both. So the second can fail after the first has succeeded.

CIAS solves this by recording every step on its own. If something fails, what remains is a tenant that is visibly broken and can be fixed, instead of one that counts as created and serves nothing.

Who may create

There are three paths, and all end in the same CIAS method:

PathWhoCheck
Admin API POST /cias/admin/tenantsa platform administratorThe role is checked on every call, also when reading. A tenant administrator may not create tenants
Self-registration of a companynobody is signed inRegistration calls the method without a role check. This variant has no HTTP endpoint
First tenant at startupthe installation itselfIf a tenant is in the bootstrap configuration and does not exist yet, CIAS creates it at startup

Why a tenant administrator may not: Creating, suspending or closing tenants is exactly what separates customers from each other. If a customer could do that, they could reach the others.

The flow

sequenceDiagram
    participant A as Admin
    participant C as CIAS
    participant DB as CIAS database
    participant P as Persistence (CDMS)
    A->>C: POST /cias/admin/tenants {key: nordbau, type: DYNAMIC}
    C->>DB: Transaction 1: key free? store as PENDING / IN_PROGRESS
    C->>P: set up (outside any transaction)
    P->>P: create database nordbau, migrate schema
    P-->>C: done
    C->>DB: Transaction 2: PROVISIONED, ACTIVE
    C-->>A: 200 with the tenant

Three reasons for this shape:

  1. Store the intent first. If the process crashes afterwards, at least something is there that you can recognize: a tenant in IN_PROGRESS with a start time.
  2. Set up without an open transaction. Creating and migrating a database can take time. If this ran inside a transaction, CIAS would hold a database connection and its locks for that whole time.
  3. Store the result afterwards, success or failure, each in its own short transaction.

What “set up” means

What happens during the setup depends on what runs in the same process:

The setup per deployment

When: CDMS persistence in the same process, CODAMAI_PERSISTENCE_TENANT_MODE=MULTI

  1. 1
    CIAS→CDMS
    asks for nordbau to be set up
  2. 2
    CDMS→Tenant DB
    database missing and creation allowed → CREATE DATABASE nordbau
  3. 3
    CDMS→Tenant DB
    migrates the schema to the state of the models

Result: The database is ready before the first customer arrives. If something fails, operations notice it when creating, not the customer on their first request.

When: CODAMAI_PERSISTENCE_TENANT_MODE=SINGLE

There is only the one database. The setup has nothing to do and reports success right away.

Result: The tenant is ACTIVE right away. In SINGLE, however, it plays no role for requests, see In SINGLE the tenant in the token does not count.

When: CIAS runs as a separate service, without CDMS persistence in the process.

CIAS has no tenant databases, so it sets nothing up and reports success right away. At startup, the log says once that this installation provisions nothing. CDMS creates the database itself on the first request for the tenant, as long as creation is allowed there.

Result: The tenant is ACTIVE right away. The customer's first request takes longer. See Databases, pools, migration.

The setup may run any number of times: An existing database is not created again, and the migration only applies missing changes. So a second attempt after a partial failure repeats work instead of creating something twice.

What can go wrong

Failure, retry, stuck rollout

When: A tenant with this key already exists, in whatever state.

CIAS creates nothing.

Result: 409 cias.tenancy.key-already-used

When: The key breaks the rules, or type is missing.

CIAS creates nothing. The message describes your own input, for example which characters are allowed.

Result: 400 cias.tenancy.invalid-request, see The tenant key

When: The database cannot be created or migrated.

  1. 1
    CIAS→CDMS
    set up → error
  2. 2
    CIAS
    stores the tenant as FAILED, standing stays PENDING, event ProvisioningFailed
  3. 3
    CIAS→Admin
    502 cias.tenancy.provisioning-failed: created, but not set up

Result: The tenant exists and is not served. Sending the same call again does not help, the key is now taken. The right move is the retry.

When: POST /cias/admin/tenants/{id}/retry-provisioning for a tenant in FAILED

  1. 1
    CIAS
    sets the rollout back to IN_PROGRESS, with a new start time
  2. 2
    CIAS→CDMS
    sets up again
  3. 3
    CIAS
    succeeds → PROVISIONED, and ACTIVE if the standing is still PENDING

Result: If the rollout is not FAILED or the tenant is closed, CIAS refuses before anything is set up: 409 cias.tenancy.illegal-transition.retry-provisioning. A tenant that was suspended in the meantime is set up by the retry but stays suspended, see The life cycle of a tenant.

When: The process dies between the two transactions. The tenant stays in IN_PROGRESS, and the retry only accepts FAILED.

  1. 1
    CIAS
    reconciliation runs every 15 minutes: Which rollouts have been IN_PROGRESS for more than 30 minutes?
  2. 2
    CIAS
    reads each match again, checks once more and sets it to FAILED, event ProvisioningFailed with the note “abandoned”

Result: After that, the normal retry works. The reconciliation can be turned off, and both times can be configured.

The reconciliation is a timer that finds rollouts that were left behind. You configure it with codamai.cias.tenancy.reconciliation.enabled, .interval (default 15 minutes) and .deadline (default 30 minutes). In a standalone CIAS it is turned on.

The deadline is generous on purpose. If it were shorter than the longest real setup, the reconciliation would abandon a rollout that is still running. That rollout could then no longer record its success, because CIAS only accepts “done” for a rollout in IN_PROGRESS. What would remain is a finished database behind a tenant in FAILED. A retry fixes that, but it is unnecessary work.

What happens with the organization of a dynamic tenant

PathOrganization in Keycloak
Self-registration (CREATE_NEW)Registration first creates the organization, alias = key, and then the tenant with its ID. After that, the person becomes a member
Admin APICIAS creates no organization. The field externalOrganizationId names an organization that already exists in Keycloak. A dynamic tenant without this field is still accepted
First tenant at startupas with the admin API, from the configuration

This way you can store a tenant and connect it to its organization later. Whether a token names the tenant depends on the alias of the organization, not on this field, see Determine the tenant of a request.

Request and response

Request
POST /cias/admin/tenants
Authorization: Bearer <token of a platform administrator>
{
  "key": "nordbau",
  "type": "DYNAMIC",
  "displayName": "Nordbau GmbH",
  "externalOrganizationId": "b7e0…"
}
Response
HTTP 200
{
  "id": "0f6c…",
  "key": "nordbau",
  "displayName": "Nordbau GmbH",
  "type": "DYNAMIC",
  "status": "ACTIVE",
  "provisioningState": "PROVISIONED",
  "externalOrganizationId": "b7e0…",
  "validFrom": null,
  "validUntil": null,
  "createdAt": "2026-09-22T09:14:03.120Z",
  "updatedAt": "2026-09-22T09:14:04.480Z"
}

displayName may be missing, then it contains the key. A validity window is not part of creating. You set it afterwards, see Suspend and close tenants, validity.

Pitfalls

Next

Sources in the code and the knowledge base
  • CIAS/cias-tenancy – TenantService (createTenant, retryProvisioning, rollOut), TenantAdministrationService (requireAdministrator), TenantAdminController (POST /cias/admin/tenants, /{id}/retry-provisioning), TenantRestDtos.CreateTenantRequest, TenantExceptionHandler
  • CIAS/cias-tenancy – Tenant (markProvisioningStarted, completeRollout)
  • CIAS/cias-tenancy – TenantReconciliationService, Tenant.isProvisioningStale; CIAS/cias-spring-boot-starter – TenantReconciliationScheduler, CiasReconciliationAutoConfiguration, CiasProperties.Tenancy.Reconciliation (interval PT15M, deadline PT30M)
  • commons-persistence – TenantProvisioningPort, DatabaseTenantProvisioningAdapter, NoOpTenantProvisioningAdapter
  • CIAS/cias-spring-boot-starter – CiasTenantProvisioningAutoConfiguration, CiasBootstrap.createFirstTenant
  • CIAS/cias-registration – RegistrationService.assignTenant
  • CIAS/cias-tenancy/docs/adr – ADR-016, ADR-020, ADR-045
Search