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:
| Path | Who | Check |
|---|---|---|
Admin API POST /cias/admin/tenants | a platform administrator | The role is checked on every call, also when reading. A tenant administrator may not create tenants |
| Self-registration of a company | nobody is signed in | Registration calls the method without a role check. This variant has no HTTP endpoint |
| First tenant at startup | the installation itself | If 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:
- Store the intent first. If the process crashes afterwards, at least something is there that you can recognize: a tenant in
IN_PROGRESSwith a start time. - 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.
- 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:
When: CDMS persistence in the same process, CODAMAI_PERSISTENCE_TENANT_MODE=MULTI
-
1CIAS→CDMSasks for
nordbauto be set up -
2CDMS→Tenant DBdatabase missing and creation allowed →
CREATE DATABASE nordbau -
3CDMS→Tenant DBmigrates 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
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.
-
1CIAS→CDMSset up → error
-
2CIASstores the tenant as
FAILED, standing staysPENDING, eventProvisioningFailed -
3CIAS→Admin502
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
-
1CIASsets the rollout back to
IN_PROGRESS, with a new start time -
2CIAS→CDMSsets up again
-
3CIASsucceeds →
PROVISIONED, andACTIVEif the standing is stillPENDING
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.
-
1CIASreconciliation runs every 15 minutes: Which rollouts have been
IN_PROGRESSfor more than 30 minutes? -
2CIASreads each match again, checks once more and sets it to
FAILED, eventProvisioningFailedwith 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
| Path | Organization 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 API | CIAS 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 startup | as 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
POST /cias/admin/tenants
Authorization: Bearer <token of a platform administrator>
{
"key": "nordbau",
"type": "DYNAMIC",
"displayName": "Nordbau GmbH",
"externalOrganizationId": "b7e0…"
}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
- The tenant key
- The lifecycle of a tenant
- How registration founds a tenant: Where the tenant comes from
- Both modules together: A new tenant, end to end