What this is about
CRMS produces reports from templates and the data of an application. It knows the same two forms as CIAS:
- embedded (
local): CRMS runs in the process of a CDMS application. The application calls CRMS through Java methods. - as a service of its own (
remote): CRMS runs in another process. The application calls it over HTTP, with the small building blockcrms-client.
CRMS always stands next to CIAS, never in its place. It manages no persons, no tenants and no roles. It asks CIAS who is calling right now and for which tenant.
The form of CRMS is a decision of its own, independent of the form of CIAS. What each form brings along in detail is in the CRMS module under Embedded or as a service of its own.
The process pictures
A process is a running program. The pictures show what sits in one process in each case.
When: CRMS and CIAS both run in the process of the CDMS application. This is how the hub backend works.
flowchart LR
F["Frontend with BFF"] -- "Bearer token" --> P
subgraph P["One process"]
direction TB
FK["Filter chain"]
C["CDMS"]
R["CRMS"]
CI["CIAS modules"]
FK --> C
C -->|"method call<br/>ReportRenderPort"| R
R -->|"method call<br/>caller, tenants"| CI
end
P --> DB[("System DB + tenant DBs<br/>CDMS, CIAS and CRMS tables")]
R --> S3[("S3 bucket<br/>final documents")]
classDef client fill:#475569,stroke:#475569,color:#fff
classDef cdms fill:#1976d2,stroke:#1976d2,color:#fff
classDef cias fill:#8e24aa,stroke:#8e24aa,color:#fff
classDef crms fill:#ffc107,stroke:#b38600,color:#1f1a00
classDef db fill:#4d7c0f,stroke:#4d7c0f,color:#fff
class F client
class C cdms
class FK,CI cias
class R crms
class DB,S3 db
Result: No network between the three. When the process is up, all three are up.
When: CRMS sits in the CDMS application, CIAS is a service of its own.
flowchart LR
F["Frontend with BFF"] -- "Bearer token" --> P
subgraph P["Service 1: CDMS application"]
direction TB
FK["Filter chain"]
TC["Tenant client"]
C["CDMS"]
R["CRMS"]
FK --> C
C -->|"method call<br/>ReportRenderPort"| R
R -->|"caller"| FK
R -->|"tenant list"| TC
end
TC -- "HTTP + service token" --> CI["Service 2: CIAS"]
P --> DB[("System DB + tenant DBs<br/>CDMS and CRMS tables")]
classDef client fill:#475569,stroke:#475569,color:#fff
classDef cdms fill:#1976d2,stroke:#1976d2,color:#fff
classDef cias fill:#8e24aa,stroke:#8e24aa,color:#fff
classDef crms fill:#ffc107,stroke:#b38600,color:#1f1a00
classDef db fill:#4d7c0f,stroke:#4d7c0f,color:#fff
class F client
class C cdms
class FK,TC,CI cias
class R crms
class DB db
Result: CDMS still calls CRMS as a method. Only what CRMS needs from CIAS and does not find in the filter chain goes over HTTP.
When: The CRMS module sits in another runtime. The CDMS application is only a client.
flowchart LR
F["Frontend with BFF"] -- "Bearer token" --> P
subgraph P["Service 1: CDMS application"]
direction TB
FK["Filter chain"]
C["CDMS"]
CC["crms-client"]
FK --> C
C -->|"ReportRenderPort"| CC
end
subgraph Q["Service 2: CRMS"]
direction TB
FK2["Filter chain<br/>token exchange"]
R["CRMS"]
FK2 --> R
end
CC -- "HTTP + the person's token" --> FK2
Q -- "HTTP + service token<br/>tenant? tenant list?" --> CI["CIAS"]
P --> DB1[("Databases of the application")]
Q --> DB2[("Database of the CRMS service")]
classDef client fill:#475569,stroke:#475569,color:#fff
classDef cdms fill:#1976d2,stroke:#1976d2,color:#fff
classDef cias fill:#8e24aa,stroke:#8e24aa,color:#fff
classDef crms fill:#ffc107,stroke:#b38600,color:#1f1a00
classDef db fill:#4d7c0f,stroke:#4d7c0f,color:#fff
class F client
class C cdms
class FK,FK2,CI cias
class CC,R crms
class DB1,DB2 db
Result: Of CRMS, the application carries only crms-client. The CRMS service checks the token itself and asks CIAS over HTTP.
In the third picture it makes no difference to the CDMS application whether CIAS is embedded in it or not. It talks to CRMS only through crms-client. The standalone CRMS service always asks CIAS over HTTP, like a CDMS service with standalone CIAS. “Service 2” can also be another CDMS application of the project in which CRMS is embedded. Then the first or second picture applies to it.
The combinations
From the point of view of one CDMS application there are three cases. Each case comes from the hub, not from a line you write by hand.
| Everything embedded | CRMS embedded, CIAS standalone | CRMS as a service of its own | |
|---|---|---|---|
| CRMS placement | local | local | remote |
| CIAS integration | EMBEDDED | REMOTE | EMBEDDED or REMOTE, irrelevant for CRMS |
| Of CRMS in the process | starter, the CRMS document archive, the format modules and the CRMS HTTP interface | as on the left | starter and crms-client, nothing else |
| Where CRMS learns the caller | from the filter chain in the same process | from the filter chain in the same process | in the CRMS service, from its own filter chain |
| Where CRMS learns the tenant list | from CIAS tenant management in the same process, method call | from the CIAS service over HTTP, with the application's service token | in the CRMS service, from the CIAS service over HTTP |
| Where the CRMS tables live | in the databases of the application | in the databases of the application | in the database of the CRMS service |
One row is missing on purpose: there is no CRMS without CIAS. CRMS needs CIAS to know the caller and to check the tenant. A system that does not sign in through CIAS gets no embedded CRMS.
Who calls whom and how
| Who asks | Whom | What | How, and with which token |
|---|---|---|---|
| CDMS code | embedded CRMS | render a report, read a document | method call to ReportRenderPort or DocumentArchivePort, no token |
| CDMS code | CRMS service | render a report, read a document | the same ports; crms-client sends HTTP with the person's token of the running request |
| CRMS | CIAS | Who is calling right now? | method call to the filter chain in its own process, in every form |
| CRMS | CIAS | Which tenants are there? (timers) | embedded a method call, standalone HTTP with a service token |
| Frontend | CRMS | edit templates, read documents | HTTP to /crms/**, with the person's token: at the application when CRMS is embedded, otherwise at the CRMS service |
| CIAS | CRMS | Which roles do you register? | embedded a method call to the bean crmsIdentityRegistry. A CRMS service of its own registers nothing |
A few words on this:
- A port is a Java interface through which your application calls CRMS. Which ones exist is under The client for a separate service.
- The filter chain is the part of CIAS that checks the token on every request and settles who the caller is. See The parties in a request.
- The roles of CRMS are names in its configuration (
templateRoleUser,templateRoleTenant,documentRoleRead…). See The roles and CRMS at start-up.
How a CDMS application calls CRMS
You let Spring hand you the port, for example in a controller of your own or in a hook, and call it with the data. The hub backend does this for its project documents:
@RestController
@RequestMapping(path = "api/rest/cms/report")
public class ReportApi {
private final ReportRenderPort renderer; // embedded CRMS or crms-client
public ReportApi(ReportRenderPort renderer) {
this.renderer = renderer;
}
// ... builds a RenderRequest from the payload and calls renderer.render(request)
}
The same code runs in both forms. Which implementation sits behind ReportRenderPort is decided by the classpath: embedded, CRMS itself, otherwise crms-client. What a render request looks like is under The render request.
A report from front to back
When: CRMS runs in the CDMS application.
sequenceDiagram
participant B as BFF
participant F as Filter chain (CIAS)
participant C as CDMS (controller or hook)
participant R as CRMS
participant DB as Tenant DB
B->>F: POST /api/rest/… + token (person from nordbau)
F->>F: check token, exchange, admit tenant nordbau
F->>C: RequestContext filled
C->>R: render(request)
Note over C,R: method call, no network
R->>F: Who is calling? person, nordbau, roles
R->>DB: resolve template, store if FINAL
R-->>C: RenderedDocument
C-->>B: document
Result: One token check, one process.
When: The CDMS application calls a CRMS service through crms-client.
sequenceDiagram
participant B as BFF
participant F as Filter chain (in CDMS)
participant C as CDMS (controller or hook)
participant K as crms-client
participant S as CRMS service
participant I as CIAS
B->>F: POST /api/rest/… + token (person from nordbau)
F->>F: check token, exchange, admit tenant nordbau
F->>C: RequestContext filled
C->>K: render(request)
K->>S: POST /crms/render + the same token
S->>S: check token, exchange it for its own client
S->>I: is nordbau served? (service token)
I-->>S: yes
S->>S: render as embedded
S-->>K: document
K-->>C: the same RenderedDocument
C-->>B: document
Result: The token is checked twice: once by the application, once by the CRMS service. Every revision and every document still names the person, not a service.
Why does the person’s token travel here, while the CIAS lookups use a service token? Because CRMS acts on behalf of the person: it stores documents in their tenant, checks their roles and writes their name into the chain. A service token would blur that. The CIAS lookups, on the other hand, ask about a fact the service itself wants to know. See CIAS as a separate service and Token exchange.
The hub settles the placement
A project has at most one CRMS module. Where you put it in the hub, that is, in which runtime (the program that runs later), settles the placement of every CDMS application of the project. There is no field for local or remote.
| CRMS module in the project? | CIAS integration of the application | Where is the CRMS module? | Result |
|---|---|---|---|
| no | – | – | no CRMS |
| yes | NONE | in another runtime | no CRMS in this application |
| yes | NONE | in its own runtime | build rejected: CRMS needs CIAS |
| yes | EMBEDDED or REMOTE | in no runtime | build rejected: the CRMS module has to be in a runtime |
| yes | EMBEDDED or REMOTE | in its own runtime | local: CRMS embedded |
| yes | EMBEDDED or REMOTE | in another runtime | remote: client of the CRMS there |
The derivation works just like the one for CIAS, see Configuration decides what runs. The two decisions are made separately. That is why all combinations above are possible, and two CDMS applications of the same project can use CRMS differently: one embeds it, the other calls it there. If the CRMS module sits in a runtime without a CDMS application, it becomes a CRMS service of its own.
The build writes codamai.crms.placement into the application.yaml and puts the matching jars into the POM. At start, CRMS checks the two against each other, see CRMS at start-up.
Same and not the same
For CRMS the same rule applies as for CIAS: the form is a decision about operation, not about what comes out. An integration test sends the same requests to an embedded and to a remote CRMS and demands the same answers.
- the same ports and the same objects, such as
RenderedDocument - the same document from the same template revision
- the same rejections with the same message keys
- the same role checks, the same person on every revision
- Outage: only remote is there
crms-unavailable - Timers: run where CRMS is embedded, so in the application or in the CRMS service
- Database: the application's or one of its own
- Client roles: those on the application's client or those on the CRMS service's client
More on the remote path is under The client for a separate service.