What this is about
Ben works for the customer nordbau. He has finished an audit and clicks “Create inspection report”. A few seconds later he has a PDF. In between, all three modules work together, and each one does exactly one thing:
- CDMS holds the data of the audit.
- CIAS says who is asking, for which tenant and with which roles.
- CRMS turns a template and data into a document, and keeps it when it is a record.
This page branches off From login to the data. Signing in, BFF and filter chain work exactly as described there. This page is about what comes after.
The whole path
This is the path when CRMS runs embedded in the application and Ben asks for a final report:
sequenceDiagram
autonumber
participant U as User
participant F as BFF
participant FK as Filter chain (CIAS)
participant A as custom code of the application
participant C as CDMS
participant R as CRMS
participant DB as Tenant DB
participant S as File storage
U->>F: clicks "Create inspection report"
F->>FK: POST /api/rest/audit/report with bearer token
FK->>FK: check token, tenant nordbau, gate, roles
FK->>A: RequestContext filled
A->>C: reads audit, questions, answers
C->>DB: SELECT (database of nordbau)
DB-->>C: rows
C-->>A: objects
A->>A: builds the payload, at most two levels
A->>R: ReportRenderPort.render(FINAL, audit-report, audit-4711, payload)
R->>FK: who is calling? (from the RequestContext)
FK-->>R: Ben, nordbau, roles
R->>DB: looks for the template (Ben, nordbau, system)
R->>R: checks the payload
R->>R: renders in the pool, seals the PDF
R->>S: stores the encrypted bytes, locked
R->>DB: appends the revision to the chain, journal
R-->>A: RenderedDocument with revision
A-->>F: PDF
F-->>U: download
The following stages walk through the path slowly.
Stage 1: up to the application
As always, the browser calls a route of its BFF. The BFF sends the request with Ben’s access token to the backend. There it first runs through the filter chain of CIAS: check the token, determine the tenant, ask the tenant gate, build the effective roles. At the end there is the RequestContext, the note for exactly this request.
This is the same path as for every other request, station by station on From login to the data. If nordbau is not served, the report ends here with 403, before a single line of the application’s code runs.
Stage 2: the application collects the data
The generator does not create a route for “Create inspection report”. That is custom code of the application: a controller of its own, like the one the hub’s reports have, or a hook. Both are Spring beans, and both can have the CRMS port injected.
-
1Application→CDMSreads the audit with everything the report shows: lists, groups, questions, answersThis runs in the RequestContext of Ben's request. That is why CDMS picks the database of
nordbau. -
2Applicationbuilds the payload from it: at most two levels, with only text, number, boolean or date at the endsSums, percentages and ratings are calculated by the application beforehand. CRMS does not calculate and loads nothing.
-
3Application→CRMScalls
ReportRenderPort.render(…)with template key, format, mode and, forFINAL, a referenceResult: From here on CRMS takes over. The application waits for the document in the same call.
This is what the call looks like:
RenderedDocument document = renderer.render(RenderRequest.finalDocument(
new TemplateKey("audit-report"), // template key
OutputFormat.PDF,
new DocumentReference("audit-4711"), // the chain: the audit's ID
payload)); // at most two levels
The reference is the ID of the business object in your application, here the ID of the audit. CRMS does not look into it; it files all final versions of the same report under it. What the payload may look like is on The payload and its check, all fields of the request on The render request.
Stage 3: CRMS asks CIAS who is calling
CRMS does not read a token. It asks CIAS for the caller, and CIAS answers from the RequestContext the filter chain has already filled:
| What CRMS gets | Where from | What CRMS needs it for |
|---|---|---|
| signed in, yes or no | RequestContext | no person, no report: crms-authentication-required |
| the person | the user ID from the token | is recorded on every final document, finds Ben’s personal templates |
| the tenant | the tenant of the request, here nordbau | which tenant templates apply, who owns the document |
| the roles | realm roles and the client’s roles | writing templates, approving, reading documents |
Rendering needs no role. Every signed-in person may render, with every template they can see. Roles only matter when templates are edited and when stored documents are read, see The roles.
Stage 4: finding the template
The request only names the key audit-report. Which edition and which revision renders is decided by CRMS. An edition is the version of a template for one scope, that is for the system, a tenant or a person. The most specific one wins:
-
1CRMS→DatabaseDoes Ben have an edition of his own (scope
USER)? It looks in the database ofnordbau -
2CRMS→DatabaseDoes
nordbauhave an edition (scopeTENANT)? Also in the database ofnordbau -
3CRMS→DatabaseIs there a system template (scope
SYSTEM)? It looks in the system database -
4CRMStakes the first edition that has a published revision in a matching languageResult: If there is none:
template-not-found. Ben never finds a template of another tenant.
Embedded, the CRMS tables sit next to those of the application, in the same databases. Which language applies and how CRMS falls back is on How CRMS finds the template and Language and time zone.
Stage 5: check, render, answer
-
CRMSFormatDoes the installation offer
PDF, and is the format module there?↳ noformat-not-offered|pdf -
CRMSModeDoes the installation store final documents?↳ no
document-revisions-disabled -
CIASCallerIs there a signed-in person?↳ no
crms-authentication-required -
CRMSTemplateDoes Ben find a published revision of
audit-report?↳ notemplate-not-found -
CRMSOutput typeDoes
PDFfit the output type of the template?↳ noformat-not-for-template|pdf|<output-type> -
CRMSPayloadDoes the payload keep the structure rules and the template's fields?↳ no
payload-rejectedwith all violations -
CRMSRenderingDoes the template run through in the pool, within the time and size limits?↳ no
render-timeout,render-output-too-large,render-capacity-exhausted… - The document is ready. For FINAL it is stored now
Up to the payload everything runs on the request’s thread, because only there is the RequestContext and with it the right database. The rendering itself runs in a bounded pool, see Execution and limits.
For FINAL the step that makes the report a record comes next:
-
1CRMSseals the PDF, if the installation seals
-
2CRMScreates a key just for this document and encrypts the bytes with it
-
3CRMS→File storagestores the bytes on WORM storage, locked until the retention endsWORM means "write once, read many": an object can be neither changed nor deleted until its date.
-
4CRMS→Databaseappends the revision to the chain of
audit-report+audit-4711, with Ben's ID, time, hashes andretainUntil, and writesdocument-storedto the journalThe document belongs to the tenant of the request and lives in its database. Without a tenant it lives in the system database. -
5CRMS→Applicationreturns the bytes, together with the new revisionResult: The bytes only come once they are stored. There is no final document that Ben has and CRMS does not.
What a revision records exactly is on What a final document is, how the chain exposes changes on The two hash chains.
Stage 6: back to Ben
The application hands the document to the BFF, the BFF to the browser. If something goes wrong, CRMS throws a CrmsException with a key, never a sentence. The application passes the key on unchanged and does not translate it. The frontend shows it in Ben’s language. The hub does it like this: it turns every CrmsException into its own error shape, with the same key and the status CRMS gives that key. The keys are on The error answer.
Embedded or as a separate service
Where CRMS runs, the hub derives from the runtime of the CRMS module, see Embedded or as a separate service. Your code calls the same port in both cases and gets the same objects and keys back. But different things happen on the way.
When: The CRMS module sits in the runtime of the application (codamai.crms.placement=local).
sequenceDiagram
autonumber
participant A as Application
participant FK as Filter chain (CIAS)
participant R as CRMS (in the process)
participant DB as Tenant DB
A->>R: render(…) as a method call
R->>FK: who is calling?
FK-->>R: Ben, nordbau, roles (from the RequestContext)
R->>DB: find template, for FINAL write the chain
R-->>A: RenderedDocument
Result: No network, no second token. Templates and documents live in the application's databases, a FINAL is written in the request's transaction.
When: The CRMS module sits in another runtime (codamai.crms.placement=remote).
sequenceDiagram
autonumber
participant A as Application
participant CL as crms-client
participant FK as Filter chain in the CRMS service
participant K as Keycloak
participant R as CRMS service
participant DB as DB of the CRMS service
A->>CL: render(…)
CL->>CL: Ben's token from the running request
CL->>FK: POST /crms/render, Authorization: Bearer (Ben's token)
FK->>K: exchange the token for the service's client
K-->>FK: token with the CRMS roles
FK->>FK: tenant from the token, gate, roles
FK->>R: RequestContext filled
R->>DB: find template, for FINAL write the chain
R-->>CL: bytes and Crms-* headers
CL-->>A: the same RenderedDocument
Result: The person travels along: the call carries Ben's token, never a service token. The service has its own filter chain, its own tenant gate and its own database.
Three consequences of the separate service:
- The roles come from the service’s client. The service exchanges Ben’s token for one for its own client. So Ben needs a document role at that client, not at the application’s client. See Token exchange.
- The tenant comes from the token only.
crms-clientforwards only theAuthorizationheader, notenantoruserheaders. The service determines the tenant as if the request came without these headers. - Without a running request there is no token. A scheduled job in the application gets
crms-authentication-required, and nothing is sent.
How the client turns answers into exceptions is on The client for the separate service.
Preview, final, transient
The path up to rendering is the same for all three modes. What happens afterwards differs:
| PREVIEW | FINAL | TRANSIENT | |
|---|---|---|---|
| What for | Ben looks at the report before he finishes it | the record, for example the finished inspection report | a working copy to pass on, for example the hub's project documents |
| Reference | no | yes, required | no |
| Template from | any scope Ben can see | any scope Ben can see | only the SYSTEM scope |
| Stored | no | yes: encrypted on WORM storage, revision in the chain with Ben's ID | no |
| In the document | visible watermark PREVIEW, marker crms-preview | nothing visible, PDF with seal if one is set up | no watermark, marker crms-transient in the metadata |
| Switched on | always | as long as the installation stores document revisions | only if the installation switches it on |
A typical flow uses two modes: Ben first clicks “Preview” (PREVIEW), checks the result and then finishes (FINAL). Both calls take exactly the same path and render with the same template; only the second one stores something. All rules of the modes are on Preview, final, transient.
What travels in which call
| Section | What travels | Who checks it |
|---|---|---|
| browser → BFF | session cookie | the BFF |
| BFF → application | Ben’s access token, plus the ID of the audit | the filter chain of CIAS |
| application → CDMS | nothing new: the same RequestContext | CDMS with roles and filters |
| application → CRMS, embedded | RenderRequest as a Java object | CRMS, with the caller from CIAS |
| application → CRMS service | POST /crms/render with Ben’s token and the request as JSON | the filter chain in the CRMS service, then CRMS |
| CRMS → database and file storage | no credentials of the person, but the installation’s connections | database and storage themselves |
Where the path ends when something is missing
| Filter chain lets it through | Template found | Payload valid | Rendering succeeds | What Ben gets |
|---|---|---|---|---|
| no | – | – | – | 401 or 403 from the filter chain, with cias.authentication.…. CRMS was never asked |
| yes | no | – | – | template-not-found, as 404 |
| yes | yes | no | – | payload-rejected with violations, as 422. The application built wrong data |
| yes | yes | yes | no | a render-… key, for example render-timeout as 504 |
| yes | yes | yes | yes | the document, for FINAL also stored |
Traps
Where to go next
- An inspection report over the years: the same report over years, with verification, legal hold and cancellation
- From login to the data: the path up to the application
- When CIAS is not reachable: for reports too
- What CRMS is and The render request
- Example: the hub’s reports: an embedded call with
TRANSIENT