This guide helps AI assistants work efficiently with the CALM Hub backend codebase.
- Language: Java 21
- Framework: Quarkus (Reactive REST, CDI) — version is set via
quarkus.platform.versionin the rootpom.xml. Use LTS releases only: upgrade to a new LTS release or a patch of the current LTS, never to a non-LTS minor release - Build Tool: Maven (via parent POM)
- Databases:
- MongoDB (production default)
- NitriteDB (embedded, standalone mode)
- Testing: JUnit 5, TestContainers
- API Docs: OpenAPI/Swagger UI
- Security: Per-namespace permissions enforced via Quarkus Security (
org.finos.calm.security); four auth modes — the default is secure (rejects all requests with 401),no-auth(open, local testing only), OIDC/Keycloak (secureprofile), and proxy-injected header (proxy-auth)
Note: Netty must stay on whatever 4.1.x version the Quarkus platform BOM manages — never add an explicit
netty-bomoverride. Quarkus 3.x applies aCleanerJava9bytecode transformation incompatible with Netty 4.2. See the comment near the top ofcalm-hub/pom.xmlfor the full rationale.
# Build & Test
../mvnw clean package # Full build
../mvnw -P integration verify # Include integration tests
../mvnw test # Unit tests only
../mvnw -Pserver-only clean package # Backend only — skips the Node/npm frontend stage
# Development Mode (Hot Reload)
# NOTE: The default profile is secure (401 on all requests). Use no-auth for local testing.
../mvnw quarkus:dev -Dquarkus.profile=no-auth # No-auth (local testing, MongoDB)
../mvnw quarkus:dev -Pstandalone # Standalone (NitriteDB) — no-auth is implicit, no Docker needed
../mvnw quarkus:dev -Pstandalone,server-only # Standalone + no frontend build — profiles compose
../mvnw quarkus:dev -Dquarkus.profile=secure # Secure mode (Keycloak)
# Docker
docker-compose up # From deploy/ - production-like
cd local-dev && docker-compose up # MongoDB for development
# Packaging
../mvnw package # Create quarkus-app/
java -jar target/quarkus-app/quarkus-run.jar # Run packaged app
# Docker Build
docker buildx build --platform linux/amd64,linux/arm64 \
-f src/main/docker/Dockerfile.jvm -t calm-hub --push .
# Docker image variants (from repo root)
bash calm-hub/build-readonly-image.sh # JVM read-only (calm-hub:read-only-static)
bash calm-hub/build-native-image.sh # Native MongoDB (calm-hub:native)
bash calm-hub/build-readonly-native-image.sh # Native read-only (calm-hub:read-only-native)
# Smoke tests — validate a running image (MODE: readonly | readwrite)
bash calm-hub/smoke-test.sh http://localhost:8080 readonly 120
bash calm-hub/smoke-test.sh http://localhost:8080 readwrite 120src/
├── main/java/org/finos/calm/
│ ├── resources/ # REST API endpoints (namespace-scoped under /calm/...)
│ ├── services/ # Business logic
│ ├── security/ # Auth: CalmHubPermissionChecker, CalmHubScopes,
│ │ # UserAccessValidator, NoAuthAuthenticationMechanism,
│ │ # ProxyAuthenticationMechanism, ReadOnlyRequestFilter
│ ├── mcp/ # MCP server (@Tool classes under mcp/tools/)
│ ├── store/ # Data access layer
│ │ ├── interfaces # Store abstractions
│ │ ├── mongo/ # MongoDB implementations
│ │ ├── nitrite/ # NitriteDB implementations
│ │ ├── github/ # Read-only stores over cloned GitHub repos (+ config/sync/registry/api/access)
│ │ ├── classpath/ # CALM meta-schemas bundled in the JAR (GitHub mode has no database)
│ │ ├── noop/ # GitHub-mode stubs for stores with nothing to back them
│ │ └── producer/ # CDI producers for store selection
│ ├── domain/ # Domain models
│ └── config/ # Configuration beans
├── test/java/ # Unit tests
└── integration-test/java/ # Integration tests (TestContainers)
Resources are namespace-scoped under /calm/.... ArchitectureResource and
NamespaceResource both use @Path("/calm/namespaces") with sub-paths such as
{namespace}/architectures. Endpoints are guarded with @PermissionsAllowed
(e.g. CalmHubScopes.READ / WRITE).
@Path("/calm/namespaces")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ArchitectureResource {
@Inject
ArchitectureStore store;
@GET
@Path("{namespace}/architectures")
@PermissionsAllowed(CalmHubScopes.READ)
public Response getArchitecturesForNamespace(
@PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace
) {
return Response.ok(new ValueWrapper<>(store.getArchitecturesForNamespace(namespace))).build();
}
}Resource types are not limited to architectures/patterns/controls — the resources/
package also covers ADR, Decorator, Domain, Flow, Interface, Standard, Timeline,
Search, and UserAccess (per-namespace and domain-level access grants).
CALM Hub supports pluggable storage backends via CDI Producers:
Configuration Property: calm.database.mode (default: mongo)
Modes:
- mongo (default) - MongoDB production storage
- standalone - NitriteDB embedded storage (no external DB needed)
- read-only -
standalone+calm.readonly=true; pre-seeded NitriteDB opened with.readOnly(true) - github - read-only; namespaces map to cloned GitHub repos (
calm.github.namespaces, onenamespace|repo|branch|groupsentry each). Writes return 501. Versions are commit SHAs, neverlatest. Needs no database.
How It Works:
- Store interfaces defined in
org.finos.calm.store - Implementations in
store/mongo/,store/nitrite/andstore/github/ store/github/reads from an in-memory registry (registry/) rebuilt after each clone or pull (sync/). Its stores extendAbstractGitHubStore(no file to read) orAbstractReadOnlyGitHubStore(reads the file at a SHA or from the clone).- A store with nothing to back it in GitHub mode gets a stub:
store/noop/for stores that need no namespace check, or astore/github/store that only doesverifyNamespace(e.g.GitHubAdrStore,GitHubDocumentStore) - Producers in
store/producer/select the implementation at runtime fromDatabaseMode. Every producer needs agithubarm: the Mongo store is gated on the mode, so a missing arm fails every request in GitHub mode with an unsatisfied resolution.
Example Producer Pattern:
@ApplicationScoped
public class ArchitectureStoreProducer {
@Inject
@ConfigProperty(name = "calm.database.mode", defaultValue = "mongo")
String databaseMode;
@Inject Instance<MongoArchitectureStore> mongoStore;
@Inject Instance<NitriteArchitectureStore> nitriteStore;
@Inject Instance<GitHubArchitectureStore> gitHubStore;
@Produces
@ApplicationScoped
public ArchitectureStore produceStore() {
if (DatabaseMode.GITHUB.equals(databaseMode)) {
return gitHubStore.get();
} else if (DatabaseMode.STANDALONE.equals(databaseMode)) {
return nitriteStore.get();
}
return mongoStore.get();
}
}Authorization is per-namespace, not role-flat. Entitlements are stored as
UserAccess records (in the active store), each granting a user read, write,
or admin action on a single namespace, plus a global_admin scope that applies
across all namespaces. Scope constants live in
org.finos.calm.security.CalmHubScopes (READ, WRITE, ADMIN,
DOMAIN_READ, DOMAIN_WRITE, GLOBAL_ADMIN). Enforcement runs through Quarkus
Security: @PermissionsAllowed on resource methods is resolved by
CalmHubPermissionChecker, which consults UserAccessValidator against the
caller's grants. See PERMISSIONS.md for the full model.
CRITICAL — never echo user-controlled input into a response body unsanitized. CodeQL's
java/xss rule (CWE-79) flags any path where a request value (@PathParam, @QueryParam,
request-body field, or exception.getMessage() derived from one) reaches a Response entity
or a log line. @Pattern bean-validation on the parameter is not a recognised barrier — CodeQL
still flags it, and content negotiation can serve the error body as text/html, making reflected
XSS exploitable.
The barrier: wrap every user-derived value with the shared OWASP sanitizer before concatenating it into a body or log message:
import static org.finos.calm.resources.ResourceValidationConstants.STRICT_SANITIZATION_POLICY;
return Response.status(Response.Status.NOT_FOUND)
.entity("Invalid namespace provided: " + STRICT_SANITIZATION_POLICY.sanitize(namespace))
.build();
logger.error("Cannot parse JSON for namespace [{}]: [{}]",
namespace, STRICT_SANITIZATION_POLICY.sanitize(request.getJson()), e);Rules:
- Sanitize in the helper, not the call site. Fix shared/private error-response helpers
(e.g.
invalidNamespaceResponse,invalidDomainResponse) so every caller is covered — not just the one method CodeQL happened to flag. - Prefer not echoing input at all. When the value adds no diagnostic value to the client,
return a generic message (see
CalmResourceErrorResponses.invalidJsonResponse, which deliberately does not echo the payload). Sanitize only when the echoed value genuinely helps the caller. - Always set
@Produces. Every endpoint should declare@Produces(MediaType.APPLICATION_JSON)(mutating endpoints with no return body included). A missing@Produceslets the entity be content-negotiated totext/html, which is what turns an echoed string into an XSS sink. - The reference implementations are
ControlResource,MappingControllerResource, andCalmResourceErrorResponses— match how they sanitize.
There are four auth modes, each selected by a separate property file:
Default (secure, no mechanism configured) — application.properties:
calm.auth.enabled=true,quarkus.oidc.tenant-enabled=false- No auth mechanism provides an identity → all requests return 401
- Forces explicit profile selection; nothing works until you choose an auth mode
No-auth (open, local development only) — application-no-auth.properties:
quarkus.oidc.tenant-enabled=false,calm.auth.enabled=falseNoAuthAuthenticationMechanismsupplies a non-anonymous identity so all permission checks pass- Never use in production; activate with
-Dquarkus.profile=no-auth
Secure (OIDC / Keycloak) — application-secure.properties:
quarkus.oidc.tenant-enabled=true,calm.auth.enabled=truequarkus.oidc.auth-server-url=https://calm-hub.finos.org:9443/realms/calm-hub-realm(requires an/etc/hostsentry mappingcalm-hub.finos.org; uses self-signed certs)- App serves SSL on port 8443 (
quarkus.http.ssl-port=8443)
Proxy-auth (proxy-injected header) — application-proxy-auth.properties:
quarkus.oidc.enabled=false,calm.auth.enabled=true- The upstream proxy injects the authenticated username in a header (default
Remote-User, overridable viacalm.security.proxy.username-header), handled byProxyAuthenticationMechanism/ProxyIdentityProvider
CALM Hub is published as four container image variants:
| Tag | Dockerfile | Runtime | Storage | Writes |
|---|---|---|---|---|
latest |
Dockerfile.jvm |
JVM (OpenJDK 21) | MongoDB (external) | ✅ Yes |
latest-native |
Dockerfile.native |
GraalVM native binary | MongoDB (external) | ✅ Yes |
latest-read-only-static |
Dockerfile.readonly-static |
JVM | Pre-seeded NitriteDB | ❌ 405 |
latest-read-only-native |
Dockerfile.readonly-native |
GraalVM native binary | Pre-seeded NitriteDB | ❌ 405 |
All images are multi-arch (linux/amd64 + linux/arm64). Native images are built per-arch on native runners (no QEMU).
How it works:
-
ReadOnlyRequestFilter(@PreMatching,@Priority(0)) intercepts every request to/calm/*.
Any mutating method (POST, PUT, DELETE, PATCH) is immediately rejected withHTTP 405andAllow: GET, HEAD, OPTIONS.
The filter readscalm.readonlyviaConfigProvider(not@ConfigProperty) so the value is respected in native images where runtime config injection may differ. -
NitriteDBConfigopens the NitriteDB file with.readOnly(true).autoCommit(false). The database file must exist (pre-seeded) — the application will throw on startup if the file is absent.
Environment variables for read-only images:
CALM_READONLY=true
QUARKUS_PROFILE=standalone
CALM_STANDALONE_DATA_DIRECTORY=/deployments/data
The read-only images use a two-stage Docker build:
Stage 1 — seed (JVM, writable)
FROM ubi9/openjdk-21
COPY quarkus-app/ + released meta-schemas (npm copy) + calm/ drafts + controls/ + interfaces/
RUN boot Quarkus in standalone (writable) mode
init script POSTs all namespaces, schemas, controls
graceful SIGTERM → NitriteDB flushes + commits (~290–450 KB)
result: /data/calmSchemas.db
Stage 2 — runtime (minimal base)
COPY --from=seed /data/calmSchemas.db ← baked as an image layer
chmod 0444 ← filesystem-level immutability
ENV CALM_READONLY=true
ENV QUARKUS_PROFILE=standalone
Why native read-only uses JVM for seeding: GraalVM native binaries cannot reliably write (commit) the H2 MVStore backing NitriteDB due to Java serialisation requirements not fully supported in native. The JVM seed stage writes the database; the native binary only reads it. Serialisation metadata for the read path is registered in META-INF/native-image/org.finos/calm-hub/serialization-config.json.
| Script | Output | Notes |
|---|---|---|
build-readonly-image.sh [--no-docker] [--no-maven] [TAG] |
calm-hub:read-only-static |
--no-docker skips local build (used by CI buildx); --no-maven reuses existing build |
build-native-image.sh [TAG] |
calm-hub:native |
GraalVM Mandrel container build; requires ≥8 GB Docker RAM |
build-readonly-native-image.sh [TAG] |
calm-hub:read-only-native |
Builds JVM jar (for seed stage) AND native binary; two-stage Docker |
smoke-test.sh [BASE_URL] [MODE] [TIMEOUT] is the shared assertion script used by all four Docker-publish CI workflows.
Arguments:
BASE_URL— defaulthttp://localhost:8080MODE—readonlyorreadwrite(default:readwrite)TIMEOUT— seconds to wait for startup (default:120)
Readiness: polls /q/swagger-ui (always available regardless of DB profile).
Assertions:
readonly:GET /api/calm/namespaces→ 200 (reads seeded data); POST/PUT/DELETE → 405 on both/api/calm/...and/calm(ReadOnlyRequestFilter)readwrite:GET /api/calm/namespaces→ 200;POST /api/calm/namespaces→ 201; name-based/calm/...POST/GET → 201/200;PUT /calm→ 403
# Read-only image (auth disabled by the standalone profile baked into the image)
docker run --rm -p 8080:8080 calm-hub:read-only-static
bash calm-hub/smoke-test.sh http://localhost:8080 readonly 120
# MongoDB image (requires running Mongo; disable auth for unauthenticated smoke-test calls)
cd calm-hub/local-dev && docker-compose up -d && cd ../..
docker run --rm -p 8080:8080 \
-e QUARKUS_MONGODB_CONNECTION_STRING=mongodb://localhost:27017 \
-e CALM_AUTH_ENABLED=false \
calm-hub:latest
bash calm-hub/smoke-test.sh http://localhost:8080 readwrite 120CALM Hub includes an experimental Model Context Protocol (MCP) server.
Dependency: io.quarkiverse.mcp:quarkus-mcp-server-http, version managed by quarkus-mcp-server-bom (imported in the root pom.xml, tracking the same quarkus.platform.version)
Endpoint: POST /mcp (HTTP Streamable JSON-RPC 2.0)
Config: calm.mcp.enabled=false (env: CALM_MCP_ENABLED)
Tool providers are in src/main/java/org/finos/calm/mcp/tools/:
ArchitectureTools, PatternTools, ControlTools, DomainTools, InterfaceTools,
NamespaceTools, SearchTools, StandardTools, TimelineTools, AdrTools.
Enable for development:
../mvnw quarkus:dev -Dcalm.mcp.enabled=true
# or
export CALM_MCP_ENABLED=true && ../mvnw quarkus:devDev-mode traffic logging: %dev.quarkus.mcp.server.traffic-logging=true (set in application.properties).
Test with curl:
curl -s -X POST http://localhost:8080/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'| Workflow file | Image tag | Trigger (path filter on main) |
|---|---|---|
docker-publish-calm-hub.yml |
latest |
calm-hub/** (excl. calm-hub/ec2/**), calm-hub-ui/** |
docker-publish-calm-hub-native.yml |
latest-native |
calm-hub/** (excl. calm-hub/ec2/**), calm-hub-ui/** |
docker-publish-calm-hub-readonly.yml |
latest-read-only-static |
calm-hub/** (excl. calm-hub/ec2/**), calm-hub-ui/**, calm/** |
docker-publish-calm-hub-readonly-native.yml |
latest-read-only-native |
calm-hub/** (excl. calm-hub/ec2/**), calm-hub-ui/**, calm/** |
calm-hub/ec2/** (the EC2 deployment scripts) is excluded from all four triggers so edits to the on-box install/deploy tooling don't kick off a native multi-arch image rebuild.
All four workflows:
- Support
workflow_dispatchwith ataginput (default: the tag above). - Run
smoke-test.shagainst a locally loaded single-arch image before pushing to Docker Hub. - Use
${{ secrets.DOCKER_USERNAME }}/calm-hubas the image namespace. - Have
cancel-in-progress: false(a publish in flight is never cancelled).
Native workflows (-native.yml) use a per-architecture build matrix (ubuntu-latest for amd64, ubuntu-24.04-arm for arm64). Each runner builds for its own arch, pushes by digest, and a final merge job assembles the multi-arch manifest via docker buildx imagetools create.
- Hot reload for Java code changes
- Accessible at http://localhost:8080
- Swagger UI at http://localhost:8080/q/swagger-ui
- Dev UI at http://localhost:8080/q/dev
- Create implementation package:
org.finos.calm.store.newmode - Implement store interfaces
- Update every Producer class: inject
Instance<NewStore>and add aDatabaseModearm - Add mode to configuration documentation
- An MCP server exposes CALM Hub operations as tools to AI clients
- Tool classes live in
mcp/tools/(~13 classes; the*Toolsones carry@Toolmethods) - Gated by
calm.mcp.enabled(defaultfalse): when disabled, every@Toolmethod returns a disabled error, though the/mcpHTTP endpoint stays reachable - Enable with
-Dcalm.mcp.enabled=trueorCALM_MCP_ENABLED=true
config/UiConfig(@ConfigMapping(prefix = "calm.ui")) holds the operator values thatGET /api/calm/ui/config(UiConfigResource) returns to CALM Hub UIUiConfigValidatorfails startup on a bad colour, URL or item, so do not relax its checks: they stop CSS injection andjavascript:links in the UI- The response leaves out a section that is not configured or not enabled. The server does not
read
SecurityIdentityunlesscalm.ui.identity.enabled=true
calm.readonly=trueopens Nitrite withreadOnly(true)and installsReadOnlyRequestFilter, which rejects mutating verbs (POST/PUT/PATCH/DELETE) on/calm/*with405 Method Not Allowedbuild-readonly-image.shpackages a static, pre-seeded read-only Docker image (Maven package + stage the released meta-schemas fromtarget/calm-schemas, thecalm/drafts and release candidates, controls, standards, patterns and interfaces + buildDockerfile.readonly-static)
- GitHub mode serves
/calm/schemasfromMETA-INF/calm-schemas/on the classpath (ClasspathCoreSchemaStore) - The frontend stage runs
npm run calm-hub:copy-schemasafternpm install. It copies every@finos/calm-schemarelease that the rootpackage.jsonpins intotarget/calm-schemas, which the build bundles. The build removes the previous copy first, and an enforcer check fails the build if the copy did not run versions.txtand eachfiles.txtundersrc/main/resources/META-INF/calm-schemas/are hand-authored. When the rootpackage.jsonadds a release, add it to them;TestClasspathCoreSchemaResourcesShouldfails until you do
nitrite/init-nitrite.shis the standard script for seeding a local Nitrite database for standalone development;nitrite/seed-readonly.shseeds the read-only variant.-usets the URL the script calls and, unlessCALM_HUB_BASE_URLor--base-urlis set, the name-based$idbase. That base must equal the server'scalm.hub.base-url. Resolve it after argument parsing (resolve_calm_hub_base_url); defaulting it at the top ignores-u- The released meta-schemas come from
target/calm-schemas/release, the npm copy that./mvnw packagemakes (npm run calm-hub:copy-schemasmakes it without Maven).calm/in the repository holds only the drafts and the release candidates, which are seeded as before. The seed data undermongo/(controls, standards, patterns, interfaces) is one directory per domain or namespace
- Located in
src/integration-test/java/ - Use TestContainers (requires Docker)
- Run via Maven only:
../mvnw -P integration verify - Cannot run from IDE unless Docker configured
- OpenAPI spec auto-generated by Quarkus
- Available at
/q/swagger-uiin dev/secure mode - Annotations:
@Operation,@APIResponse, etc.
CRITICAL: JaCoCo enforces 90% line coverage per class. CI runs mvn clean verify which includes the JaCoCo coverage check. Any class below 90% will fail the build.
# Run the same check CI uses — always run this before pushing changes
../mvnw clean verifyExclusions (from pom.xml): **/*Builder.*, **/*CalmResourceErrorResponses.*, **/*Constants.*, **/*NamespaceStandardSummary.*, **/*ArchitectureRequest.*, **/config/**/*, and **/domain/**/* are excluded from the coverage check.
If coverage drops below 90% for a class you've modified, add tests for uncovered error paths (catch blocks, edge cases) until the threshold is met. Check the JaCoCo report at target/site/jacoco/ for details on uncovered lines.
../mvnw test # All unit tests
../mvnw test -Dtest=ClassName # Specific test class../mvnw -P integration verify # Requires Docker runningsrc/test/java/- Unit tests (fast, no containers)src/integration-test/java/- Integration tests (TestContainers)
All tests must follow these conventions. The Pattern and Architecture tests are the reference implementations — Controls and any new resource types must match them.
- Test class:
Test<ClassName>Should(e.g.TestPatternResourceShould,TestMongoPatternStoreShould) - Test methods:
snake_casedescriptive names (e.g.return_a_400_when_an_invalid_format_of_namespace_is_provided_on_get_patterns)
Mock field naming: Prefix store mocks with mock — e.g. mockPatternStore, mockControlStore, not patternStore or controlStore.
@InjectMock
PatternStore mockPatternStore;Parameterized tests for exception/status-code scenarios: Use @ParameterizedTest + @MethodSource to consolidate multiple error-path tests into one:
static Stream<Arguments> provideParametersForGetPatternVersionTests() {
return Stream.of(
Arguments.of("finos", new NamespaceNotFoundException(), 404),
Arguments.of("finos", new PatternNotFoundException(), 404),
Arguments.of("finos", null, 200)
);
}
@ParameterizedTest
@MethodSource("provideParametersForGetPatternVersionTests")
void respond_correctly_to_get_pattern_versions_query(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws Exception {
// ...
}Input validation tests: Every endpoint with @Pattern-annotated path parameters must have corresponding 400-validation tests:
- Invalid domain format (e.g.
invalid_domainwith underscores) → 400 withDOMAIN_NAME_MESSAGE - Invalid namespace format (e.g.
fin_os) → 400 withNAMESPACE_MESSAGE - Invalid version format (e.g.
1.0.invalid0) → 400 withVERSION_MESSAGE
@Test
void return_a_400_when_an_invalid_format_of_namespace_is_provided_on_get_patterns() {
given()
.when()
.get("/calm/namespaces/fin_os/patterns")
.then()
.statusCode(400)
.body(containsString(NAMESPACE_MESSAGE));
}Import validation constants from ResourceValidationConstants:
import static org.finos.calm.resources.ResourceValidationConstants.DOMAIN_MESSAGE;
import static org.finos.calm.resources.ResourceValidationConstants.VERSION_MESSAGE;
import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
import static org.finos.calm.resources.ResourceValidationConstants.USERNAME_MESSAGE;Inner interfaces for type-safe mocking — never use @SuppressWarnings("unchecked"):
private interface DocumentFindIterable extends FindIterable<Document> {
}
private interface DocumentMongoCollection extends MongoCollection<Document> {
}
@BeforeEach
void setup() {
patternCollection = Mockito.mock(DocumentMongoCollection.class);
// ...
}
private FindIterable<Document> mockFindIterable() {
return Mockito.mock(DocumentFindIterable.class);
}Field naming: Use short names without the mongo prefix:
counterStore(notmongoCounterStore)domainStore(notmongoDomainStore)namespaceStore(notmongoNamespaceStore)
Shared fixture helpers: Extract common Document setup into reusable helpers:
setupInvalidPattern()— mock find returning null for not-found scenariosmockSetupPatternDocumentWithVersions()— mock a full document with versionssetupPatternVersionDocument()— build a Document fixture
Nitrite tests use plain Mockito (not @QuarkusTest). Follow the same patterns as Mongo store tests where applicable.
Constants: Always use private static final (not private final):
private static final String NAMESPACE = "finos";
private static final int PATTERN_ID = 42;- Create resource class in
resources/ - Implement service logic in
services/ - Add store methods if needed in
store/ - Write unit tests in
test/ - Add integration test in
integration-test/ - Document with OpenAPI annotations
- Declare
@Produces(MediaType.APPLICATION_JSON)on every method, and sanitize any user-derived value echoed into a response body or log withSTRICT_SANITIZATION_POLICY.sanitize(...)(see Preventing Cross-Site Scripting (XSS) in Responses) - A new mutating endpoint gets audit-trail coverage automatically — no action needed —
as long as it follows the existing
{namespace|domain}/.../{id}[/versions/{version}]path convention and, for server-generated IDs, returnsResponse.created(...)with aLocationthat embeds the new ID (see Audit Logging). A one-line addition is only needed inAuditRequestFilter'sRESOURCE_CLASS_TO_ENTITY_TYPEmap (andLocationSegmentParserif the new entity is server-ID-generated) when introducing a genuinely new resource type, not a new endpoint on an existing one.
Every mutating (POST/PUT/DELETE) request that completes with a 2xx or 401/403 status is
recorded to the auditLogs collection (and, if enabled, a structured log line under the
org.finos.calm.audit category) by security/AuditRequestFilter.java, a post-matching
ContainerResponseFilter — deliberately not @PreMatching like ReadOnlyRequestFilter,
since it needs UriInfo.getPathParameters() resolved after routing. See that class's
javadoc for the full entity-resolution design (path params → Location-header fallback →
staged AuditContext for the handful of body-derived endpoints). store/AuditLogStore.java
is append-only (no update/delete) and follows the standard Mongo/Nitrite dual-backend
pattern. Toggle independently with calm.audit.store.enabled / calm.audit.log.enabled /
calm.audit.capture-source-ip (all in application.properties). No REST API exposes the
trail in this iteration — read it via direct DB access or ops tooling.
GitHub storage mode (calm.database.mode=github) has no database, so it does not persist the
trail: GitHubAuditLogStore.query() always returns an empty list, and the structured log line
is the only record. GitHubStartupInitializer logs a warning about this at startup. Route the
org.finos.calm.audit category to a durable sink if you need the trail in GitHub mode.
- Create package:
org.finos.calm.store.mybackend - Implement all store interfaces
- Update all Producer classes to include new implementation (
Instance<>+ aDatabaseModearm) - Add a producer test for the new mode
- Document in README.md
- Start dev MongoDB:
cd local-dev && docker-compose up - Connection details in
application.properties - Database name:
calm-hub(default)
- Generate certificates (see README.md)
- Add an
/etc/hostsentry mappingcalm-hub.finos.orgto localhost (the OIDCauth-server-urluses this hostname, notlocalhost) - Start Keycloak:
cd keycloak-dev && docker-compose up - Login to Keycloak: https://calm-hub.finos.org:9443 (admin/password)
- Switch to
calm-hub-realm - Use
demouser for testing - Run app:
../mvnw quarkus:dev -Dquarkus.profile=secure(serves SSL on port 8443)
pom.xml- Maven dependencies and build configsrc/main/resources/application.properties- Default (secure, rejects all requests) configsrc/main/resources/application-no-auth.properties- No-auth profile (local testing only, no IdP)src/main/resources/application-secure.properties- Secure (OIDC/Keycloak) profile configsrc/main/resources/application-proxy-auth.properties- Proxy-auth profile config (proxy-injectedRemote-Userheader)
Do not add a second application.properties under src/test/resources. One
used to exist and, depending on the machine, either it or the main file won
wholesale rather than merging per-property — so tests silently lost either the
audit-disable settings (writing to whatever MongoDB the developer had running) or
the auth and security-header settings (7 failures that passed in isolation). All
test-only configuration lives in the %test. block of
src/main/resources/application.properties; add new test settings there.
When you add a SchemaMigrationStep, update mongo/init-mongo.js in the same change.
That script seeds a fresh database directly in the current storage shape and pins
LATEST_SCHEMA_VERSION, so SchemaMigrationRunner skips every step on first startup.
Two consequences: the seed data must already be in the shape your new step produces, and
because step 0 is skipped along with the rest, the script creates the whole index
inventory itself — a duplicate of MongoIndexInitializationStep that has to be kept in
step. Miss the version bump and the seeded stack runs migrations it does not need; miss
the index list and duplicate-prevention silently disappears. See
ADR 0001.
PERMISSIONS.md- Per-namespace permission model referencedecisions/- Architecture Decision Records for calm-hub's own backend design (not the CALM ADR resource type). Seedecisions/README.mdfor the format; check each ADR'sStatusfield before assuming it describes current behaviour rather than a proposed/pending design.
calm-hub depends on:
└── cli (via parent POM, but loosely coupled)
CALM Hub is largely independent - it's a standalone REST API server.
- TestContainers Errors: Ensure Docker is running before integration tests
- Port Conflicts: Check if port 8080 is free (or change
quarkus.http.port) - MongoDB Connection: Start MongoDB before dev mode (unless using
-Pstandalonefor NitriteDB)- IMPORTANT: Use
-Pstandalone(Maven profile), not-Dquarkus.profile=standalone. Dev mode does not apply aquarkus.profileset with-Don the Maven command line; the Maven profile injectsquarkus.profile=standalonevia the plugin's ownsystemPropertiesconfiguration. Other-Dflags reach the app. - Standalone turns off all dev services, so it needs no Docker. To start the Grafana LGTM
observability dev service, add
-Dquarkus.devservices.enabled=true(needs Docker). OpenTelemetry stays off until you also setCALM_OTEL_DISABLED=false.
- IMPORTANT: Use
- Certificate Issues: Use exact CN in URLs when using self-signed certs
- Profile Selection: Remember to pass
-Dquarkus.profile=securefor secure mode - Reflected XSS in error bodies/logs: Concatenating a raw
@PathParam/@QueryParam/request field into aResponseentity or log message triggers CodeQLjava/xss.@Patternvalidation is not a barrier — wrap the value inSTRICT_SANITIZATION_POLICY.sanitize(...). See Preventing Cross-Site Scripting (XSS) in Responses. -Pserver-onlydoes not build or refresh the bundled UI: it skips the Node/npm frontend stage entirely (see README.md). A fresh clone built with this profile ships no UI at all; never use it for a release, Docker image, or CI build that needs to serve the UI. It (and-Dskip.npm) also skips the meta-schema copy, so aftercleanthe jar has no bundled meta-schemas and GitHub mode serves none.
If using Java 24 or later, you may see thread-local access warnings during tests:
java.lang.IllegalAccessError: module java.base does not open java.lang to unnamed module
These warnings are harmless and don't affect test results. To suppress them, add JVM options:
../mvnw test -Dquarkus.args="--add-opens java.base/java.lang=ALL-UNNAMED"Recommended: Use Java 21 LTS for development (as specified in pom.xml).
- Use
@ConfigPropertyto inject config values - Profile-specific:
%profile.property.name=value - Environment overrides:
PROPERTY_NAME=value
- Use
@Injectfor dependency injection - Scopes:
@ApplicationScoped,@RequestScoped - Producers:
@Producesfor factory methods
@Pathfor routing@GET,@POST,@PUT,@DELETEfor HTTP methods@PathParam,@QueryParamfor parameters
- README.md - Detailed setup and deployment guide
- Quarkus Docs - Framework documentation
- Root README - Monorepo overview
- Swagger UI: http://localhost:8080/q/swagger-ui (when running)