Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CE-RISE Hex Core Service Documentation

This site contains the technical documentation for the CE-RISE hex-core-service.

Use the navigation sidebar to access architecture, API, configuration, deployment, adapter contract, and operations documentation.


First 5 Minutes

This quickstart verifies that the service is reachable and can process one validation request.

1. Start the service

Run your deployed container (or local instance) with the required environment variables configured.

2. Check health

curl http://localhost:8080/admin/health

Expected response:

{"status":"ok"}

3. Check loaded models

curl http://localhost:8080/models

Pick one model and version from the response.

4. Validate a payload

curl -X POST "http://localhost:8080/models/<model>/versions/<version>:validate" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": {
      "id": "example-001",
      "name": "example record"
    }
  }'

Expected response shape:

{
  "passed": true,
  "results": []
}

If your auth mode is none, you can omit the Authorization header.


Funded by the European Union under Grant Agreement No. 101092281 — CE-RISE.
Views and opinions expressed are those of the author(s) only and do not necessarily reflect those of the European Union or the granting authority (HADEA). Neither the European Union nor the granting authority can be held responsible for them.

CE-RISE logo

© 2026 CE-RISE consortium.
Licensed under the European Union Public Licence v1.2 (EUPL-1.2).
Attribution: CE-RISE project (Grant Agreement No. 101092281) and the individual authors/partners as indicated.

NILU logo

Developed by NILU (Riccardo Boero — ribo@nilu.no) within the CE-RISE project.

Architecture

Overview

The CE-RISE Hex Core Service follows a strict hexagonal (ports and adapters) architecture. The domain and use-case logic in crates/core has no knowledge of HTTP, databases, or any specific IO provider. All external interactions are mediated through port traits.

Architecture Views

Digital Passport Interaction View

Digital Passport Interaction View

Digital Passport interaction flow showing both external consumption and internal processing: clients submit/query records, the core resolves model artifacts from registry/catalog sources, validates payloads, and reads/writes through configured backend adapters.

Deployment View

Hex Core Service Deployment View

Deployment-oriented architecture view of the hex-core service, showing inbound interfaces, core orchestration, outbound adapters, and runtime dependencies in one deployable unit.

The hexagonal architecture pattern used in this service is also illustrated in the following diagram:

                    ┌──────────────────────────────────────┐
                    │           INBOUND ADAPTERS           │
                    │  REST API (axum)  │  CLI  │  Tests   │
                    └──────────┬──────────────────────────-┘
                               │  calls inbound port traits
                    ┌──────────▼───────────────────────────-┐
                    │             CORE (crate)              │
                    │  ┌────────────────────────────────┐   │
                    │  │   Use Cases (implementations)  │   │
                    │  │  ValidateUseCase               │   │
                    │  │  RecordUseCase                 │   │
                    │  │  EnrichUseCase (opt.)          │   │
                    │  └────────────┬───────────────────┘   │
                    │               │  calls outbound port traits
                    └──────────────-│─────────────────────--┘
                                    │
              ┌─────────────────────┼──────────────────────┐
              │                     │                      │
   ┌──────────▼──────┐  ┌───────────▼──────┐  ┌────────────▼──────┐
   │ ArtifactRegistry│  │  ValidatorPort   │  │  RecordStorePort  │
   │ (catalog + URL  │  │  (SHACL / JSON   │  │  (HTTP IO adapter │
   │   fetch helper) │  │   Schema / OWL)  │  │   memory / db)    │
   └─────────────────┘  └──────────────────┘  └───────────────────┘

Layer dependency rules

The architecture enforces strict dependency constraints to maintain separation of concerns:

LayerAllowed dependenciesForbidden
core/domainstd, serde, thiserrorEverything else
core/portscore/domain, async-traitAny I/O
core/usecasescore/domain, core/portsAny I/O, HTTP, DB
registrycore/ports, reqwest, tokiocore/usecases
apicore/ports, axum, tower, jsonwebtokenDirect DB/IO
validator-*core/ports, validator-specific libsHTTP, DB
io-*core/ports, adapter-specific libsCore use cases

These rules ensure that:

  • The core domain remains pure and testable
  • Business logic has no coupling to infrastructure
  • Adapters can be swapped without affecting the core
  • Dependencies flow inward toward the domain

Layers

  • Domain (crates/core/src/domain) — entities, value objects, error types. No I/O.
  • Ports (crates/core/src/ports) — inbound use-case traits and outbound adapter traits.
  • Use cases (crates/core/src/usecases) — orchestration logic implementing inbound ports.
  • Registry (crates/registry) — catalog-backed ArtifactRegistryPort with URL artifact fetching.
  • REST adapter (crates/api) — axum HTTP server implementing the inbound interface.
  • IO adapters (crates/io-memory, crates/io-http) — RecordStorePort implementations.
  • Validators (crates/validator-jsonschema, crates/validator-shacl) — ValidatorPort implementations.

Key design decisions

Use-case error behavior (implemented)

  • ValidateUseCaseImpl propagates validator execution failures as domain errors.
  • RecordUseCaseImpl blocks writes when merged validation report fails.
  • RecordUseCaseImpl also propagates validator execution failures (they are not ignored).

API Reference

Base URL

https://<host>/

Authentication

All endpoints except GET /admin/health require authentication.

Authorization: Bearer <access_token>

Auth modes and integration patterns are described in Authentication.

End-to-End Operation Examples

Validate

POST /models/{model}/versions/{version}:validate

Example request:

curl -X POST "https://<host>/models/re-indicators-specification/versions/0.0.3:validate" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": {
      "id": "record-001",
      "name": "example"
    }
  }'

Example response (200):

{
  "passed": true,
  "results": []
}

Create

POST /models/{model}/versions/{version}:create

Example request:

curl -X POST "https://<host>/models/re-indicators-specification/versions/0.0.3:create" \
  -H "Authorization: Bearer <token>" \
  -H "Idempotency-Key: 7f8d4d5e-1fcb-4eab-a4bb-2af7ca0f7f12" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": {
      "id": "record-001",
      "name": "example"
    }
  }'

Example response (200):

{
  "id": "record-001",
  "model": "re-indicators-specification",
  "version": "0.0.3",
  "payload": {
    "id": "record-001",
    "name": "example"
  }
}

Query

POST /models/{model}/versions/{version}:query

Example request:

curl -X POST "https://<host>/models/re-indicators-specification/versions/0.0.3:query" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": {
      "where": [
        { "field": "id", "op": "eq", "value": "record-001" },
        { "field": "payload.record_scope", "op": "eq", "value": "product" }
      ],
      "sort": [
        { "field": "created_at", "direction": "desc" }
      ],
      "limit": 25,
      "offset": 0
    }
  }'

Example response (200):

{
  "records": [
    {
      "id": "record-001",
      "model": "re-indicators-specification",
      "version": "0.0.3",
      "payload": {
        "id": "record-001",
        "name": "example"
      }
    }
  ]
}

Supported query operators:

  • eq
  • ne
  • in
  • contains
  • exists
  • gt
  • gte
  • lt
  • lte

Field path rules:

  • Storage/root fields: id, model, version, created_at, updated_at
  • Payload fields under payload, for example payload.record_scope
  • Array positions may be addressed with brackets, for example payload.applied_schemas[0].schema_url

Query v1 constraints:

  • where is AND-only
  • no OR groups or nested boolean trees
  • no backend-specific raw query fragments

Public Introspection

List available models

GET /models

Example response (200):

{
  "models": [
    {
      "id": "re-indicators-specification",
      "version": "0.0.3"
    }
  ]
}

Get model artifacts

GET /models/{model}/versions/{version}/schema
GET /models/{model}/versions/{version}/shacl
GET /models/{model}/versions/{version}/owl
GET /models/{model}/versions/{version}/route

Returns raw artifact content when available.

OpenAPI document

GET /openapi.json

Admin Endpoints

MethodPathDescription
GET/admin/healthLiveness probe
GET/admin/versionService/OpenAPI version probe
GET/admin/models/countNumber of currently indexed models
GET/admin/readyReadiness probe
GET/admin/statusRuntime status
GET/admin/metricsPrometheus metrics (METRICS_ENABLED=true)
POST/admin/registry/refreshReload registry catalog/artifacts

Version response shape:

{
  "service": "hex-core-service",
  "service_version": "0.1.0",
  "openapi_version": "0.0.2"
}

Model count response shape:

{
  "models_count": 3
}

Refresh response shape:

{
  "refreshed_at": "2026-03-03T18:12:45Z",
  "models_found": 3,
  "errors": []
}

Error Format

All errors use a JSON envelope:

{
  "code": "MODEL_NOT_FOUND",
  "message": "Model re-indicators-specification v0.0.3 not found in registry",
  "details": null
}

Common mappings:

CodeHTTP
MODEL_NOT_FOUND404
VALIDATION_FAILED422
NOT_ROUTABLE422
IDEMPOTENCY_CONFLICT409
STORE_ERROR502
REGISTRY_ERROR502
VALIDATOR_ERROR500
INTERNAL_ERROR500

Authentication

This page explains authentication options for service operators and API consumers.

Overview

hex-core-service supports three runtime auth modes selected with AUTH_MODE:

  • jwt_jwks
  • forward_auth
  • none (isolated non-production only)

All endpoints except GET /admin/health require authentication.

Auth Mode by Scenario

ScenarioRecommended modeWhy
Public/production API with OIDC providerjwt_jwksService validates JWT directly against JWKS
Enterprise gateway already handles auth (OIDC/SAML/LDAP/introspection)forward_authGateway is source of truth; service trusts injected identity headers
Local dev or isolated integration testingnoneLets you run flows without IdP/gateway setup

Mode 1: jwt_jwks (default)

Direct bearer JWT validation in this service.

Required variables:

  • AUTH_MODE=jwt_jwks
  • AUTH_JWKS_URL
  • AUTH_ISSUER
  • AUTH_AUDIENCE
  • optional AUTH_JWKS_REFRESH_SECS (default 3600)

Expected request header:

Authorization: Bearer <token>

Current claim mapping:

  • subject: sub
  • roles: realm_access.roles
  • scopes: scope (space-separated)

Mode 2: forward_auth

Use this when an upstream gateway/proxy/mesh authenticates users and forwards identity headers.

Required variable:

  • AUTH_MODE=forward_auth

Header mapping variables (defaults):

  • AUTH_FORWARD_SUBJECT_HEADER=x-auth-subject
  • AUTH_FORWARD_ROLES_HEADER=x-auth-roles
  • AUTH_FORWARD_SCOPES_HEADER=x-auth-scopes
  • AUTH_FORWARD_TENANT_HEADER (optional)
  • AUTH_FORWARD_TOKEN_HEADER (optional)

Example forwarded headers:

x-auth-subject: user-123
x-auth-roles: admin,qa
x-auth-scopes: records:read records:write
x-auth-tenant: tenant-a

Security requirement:

  • accept these headers only from trusted upstream infrastructure.

Mode 3: none (non-production only)

Disables authentication checks and injects a fixed local identity.

Required variables:

  • AUTH_MODE=none
  • AUTH_ALLOW_INSECURE_NONE=true

Optional identity variables:

  • AUTH_NONE_SUBJECT (default dev-anonymous)
  • AUTH_NONE_ROLES
  • AUTH_NONE_SCOPES
  • AUTH_NONE_TENANT

Safety behavior:

  • startup fails if AUTH_ALLOW_INSECURE_NONE is not explicitly true.

Integration Notes

OIDC providers

Works with Keycloak and other OIDC providers that expose JWKS and compatible claims.

SAML environments

Recommended pattern:

  1. Handle SAML in gateway/identity broker.
  2. Use forward_auth into this service.

OAuth2 opaque tokens

Recommended pattern:

  1. Introspect token in gateway.
  2. Forward resolved identity via forward_auth.

Quick Troubleshooting

  • 401 Unauthorized: invalid/missing token, issuer/audience mismatch, expired token.
  • 403 Forbidden: identity authenticated but not allowed by upstream policy.
  • forward_auth issues: header names do not match configured env vars.

Model Onboarding

This page explains how to add a new model/version so it becomes available through the service API.

Artifact References

Each model version may publish any subset of these artifacts:

  • route.json (required only for routable create/query/dispatch operations)
  • schema.json (optional, for JSON Schema validation)
  • shacl.ttl (optional, for SHACL validation)
  • owl.ttl (optional, for OWL validation)
  • openapi.json (optional, for model-level API description)

Validation-only models do not need route.json.

Catalog Entry Format

hex-core-service reads model definitions from a catalog (URL/file/inline JSON).
Each entry should point directly to the artifacts it publishes.

Example catalog.json:

{
  "models": [
    {
      "model": "re-indicators-specification",
      "version": "0.0.3",
      "route_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/route.json",
      "schema_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/schema.json",
      "shacl_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/shacl.ttl"
    }
  ]
}

Rules:

  • model + version must be unique in the catalog.
  • Each entry must declare at least one artifact reference.
  • Artifact references must be direct runtime-fetchable file URLs.
  • If HTTPS enforcement is enabled, catalog/artifact URLs must be HTTPS.
  • Artifact hosts must be permitted by REGISTRY_ALLOWED_HOSTS.

Onboarding Flow

  1. Publish the artifact files required by your runtime use case.
  2. Add the model/version entry to your catalog source.
  3. Ensure the service is configured to load that catalog.
  4. Trigger registry refresh:
curl -X POST http://localhost:8080/admin/registry/refresh \
  -H "Authorization: Bearer <admin-token>"
  1. Confirm the model/version is now available:
curl http://localhost:8080/models

Verification Checklist

  • GET /models returns your model/version.
  • GET /models/{model}/versions/{version}/route returns 200 only for routable models.
  • Optional artifacts (schema, shacl, owl) return 200 if expected.
  • POST /models/{model}/versions/{version}:validate returns a valid response for a known-good payload.

Typical Onboarding Errors

  • 404 model not found: catalog not refreshed or wrong model/version pair.
  • Refresh returns entry errors: invalid artifact URL, unreadable artifact, or blocked host/HTTP policy.
  • Validation missing expected checks: corresponding artifact (schema.json, shacl.ttl, owl.ttl) not published.

SHACL Validation

This page describes how SHACL validation is currently executed in hex-core-service.

Scope and Current Behavior

SHACL is the preferred validation path for model payloads, but the current implementation is profile-based.

  • Adapter crate: crates/validator-shacl
  • Core contract: ValidatorPort
  • Runtime result kind: ValidationKind::Shacl

At this stage, the SHACL adapter validates payload constraints aligned with the dp-record-metadata profile. It does not yet execute a full generic SHACL engine over arbitrary RDF graphs.

When SHACL Runs

For POST /models/{model}/versions/{version}:validate:

  1. Core resolves artifacts for (model, version) from the registry.
  2. Core executes configured validators.
  3. SHACL validation runs when a shacl.ttl artifact is available in the resolved ArtifactSet.

If shacl.ttl is absent, SHACL is skipped by orchestration and does not block other validator results.

Supported Checks (Current Adapter)

The current SHACL adapter enforces:

  • record_scope must be one of product or material
  • related_passports[*].relation_type must be one of:
    • derived_from
    • contributes_to
    • split_from
    • merged_into
    • recycled_into
    • manufactured_from
  • metadata_versioning.metadata_created must be RFC3339 timestamp
  • metadata_versioning.metadata_modified must be RFC3339 timestamp
  • applied_schemas[*].composition_info.sequence_order must be integer
  • applied_schemas[*].schema_usage.completeness_percentage must be numeric
  • applied_schemas[*] is treated as closed shape for keys:
    • schema_reference
    • schema_usage
    • composition_info

Violations are returned as severity = error, with a JSON path and message.

Registry Requirements

To enable SHACL validation for a model version:

  • Include a catalog entry for that (model, version).
  • Ensure the catalog entry declares a shacl_url.
  • route_url is only needed if the same model must also support routable operations.

Example explicit SHACL artifact URL:

https://codeberg.org/CE-RISE-models/<model>/src/tag/pages-v<version>/generated/shacl.ttl

API Result Shape

Validation responses merge all validator outputs:

{
  "passed": false,
  "results": [
    {
      "kind": "shacl",
      "passed": false,
      "violations": [
        {
          "path": "$.record_scope",
          "message": "record_scope must be one of: product, material",
          "severity": "error"
        }
      ]
    }
  ]
}

Refresh and Operations

  • Update model artifacts or catalog source.
  • Trigger POST /admin/registry/refresh.
  • New SHACL artifacts become active after successful atomic index swap.

Known Limitations

  • Not a full RDF/SHACL graph validation engine yet.
  • Focused on currently supported SHACL profile checks.
  • OWL validation is documented separately in the OWL validator sections.

Adapter Contract

This document specifies the integration contract for IO adapters, validators, and enrichers. All adapters integrate via port traits defined in crates/core/src/ports/outbound/.


Overview

The hex-core-service uses port traits to define contracts between the core and external adapters. Adapters implement these traits to provide pluggable functionality without coupling the core to specific implementations.

Adapter Types

Adapter TypePort TraitPurpose
IO AdapterRecordStorePortRead/write business records to external storage
ValidatorValidatorPortValidate payloads against model artifacts
EnricherEnricherPortEnrich records with external data (optional)
RegistryArtifactRegistryPortResolve versioned model artifacts

RecordStorePort (IO Adapter Contract)

Trait Definition

#![allow(unused)]
fn main() {
// crates/core/src/ports/outbound/record_store.rs
#[async_trait::async_trait]
pub trait RecordStorePort: Send + Sync {
    async fn write(
        &self,
        ctx:    &SecurityContext,
        record: Record,
    ) -> Result<RecordId, StoreError>;

    async fn read(
        &self,
        ctx: &SecurityContext,
        id:  &RecordId,
    ) -> Result<Record, StoreError>;

    async fn query(
        &self,
        ctx:    &SecurityContext,
        filter: serde_json::Value,
    ) -> Result<Vec<Record>, StoreError>;
}
}

Method Specifications

write

Purpose: Persist a new or updated record.

Parameters:

  • ctx — Security context containing user identity, roles, and access token
  • record — Complete record with ID, model, version, and payload

Returns:

  • Ok(RecordId) — The persisted record’s ID (may be generated or echoed)
  • Err(StoreError) — Storage failure, conflict, or authorization error

Requirements:

  • Must support idempotency via Idempotency-Key (implementation-specific)
  • Must validate user authorization before persisting
  • Should preserve record metadata (model, version)
  • Must return StoreError::IdempotencyConflict if key is reused with different payload

Security:

  • Adapter receives SecurityContext::raw_token and must forward it to backend services
  • Adapter must never log or persist the access token

read

Purpose: Retrieve a single record by ID.

Parameters:

  • ctx — Security context
  • id — Record identifier

Returns:

  • Ok(Record) — The requested record
  • Err(StoreError::NotFound) — Record does not exist or user lacks access
  • Err(StoreError) — Other storage error

Requirements:

  • Must enforce authorization (user can only read records they have access to)
  • Should be fast (single lookup, not a scan)

query

Purpose: Search for records matching filter criteria.

Parameters:

  • ctx — Security context
  • filter — Canonical JSON query expression defined by hex-core

Returns:

  • Ok(Vec<Record>) — Matching records (may be empty)
  • Err(StoreError) — Storage error or invalid filter

Requirements:

  • Must enforce authorization (filter results to user’s scope)
  • Must implement the canonical query dialect defined below
  • Must support limit and offset
  • May return empty results if no matches found

Canonical query dialect

Backend adapters must accept POST /records/query with:

{
  "filter": {
    "where": [
      { "field": "id", "op": "eq", "value": "record-001" },
      { "field": "payload.record_scope", "op": "eq", "value": "product" }
    ],
    "sort": [
      { "field": "created_at", "direction": "desc" }
    ],
    "limit": 50,
    "offset": 0
  }
}

Dialect rules:

  • where is an AND-only list of predicates
  • sort is optional
  • limit is optional and should default to an implementation-defined safe value
  • offset is optional and defaults to 0
  • Results must be returned as full Record objects

Supported operators:

  • eq
  • ne
  • in
  • contains
  • exists
  • gt
  • gte
  • lt
  • lte

Field path rules:

  • Storage/root fields: id, model, version, created_at, updated_at
  • Payload fields: dotted paths under payload, for example payload.record_scope
  • Array addressing may use zero-based brackets, for example payload.applied_schemas[0].schema_url

Required semantics:

  • eq, ne, gt, gte, lt, lte compare scalar values
  • in expects value to be an array
  • contains is for substring containment on strings or membership in arrays
  • exists expects boolean value

Out of scope for v1:

  • OR groups
  • nested boolean trees
  • joins
  • aggregates
  • backend-native raw query fragments

Error behavior:

  • Invalid query shape should map to a client error in the backend HTTP API
  • Unsupported field/operator combinations must be rejected explicitly, not ignored silently
  • Backends should document any storage-specific limits, but must preserve the canonical wire shape

Error Types

#![allow(unused)]
fn main() {
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
    #[error("not found")]
    NotFound,
    
    #[error("idempotency conflict")]
    IdempotencyConflict,
    
    #[error("unauthorized")]
    Unauthorized,
    
    #[error("connection failed: {0}")]
    ConnectionFailed(String),
    
    #[error("internal: {0}")]
    Internal(String),
}
}

Implementation Examples

  • crates/io-memory — In-memory HashMap (for testing and local development)
  • crates/io-http — HTTP client to external IO Adapter Service

Versioned IO Adapter OpenAPI contract (current source of truth for HTTP paths/methods):

  • crates/io-http/src/io_adapter_openapi.json

ValidatorPort (Validator Contract)

Trait Definition

#![allow(unused)]
fn main() {
// crates/core/src/ports/outbound/validator.rs
#[async_trait::async_trait]
pub trait ValidatorPort: Send + Sync {
    fn kind(&self) -> ValidatorKind;

    async fn validate(
        &self,
        artifacts: &ArtifactSet,
        payload:   &serde_json::Value,
    ) -> Result<ValidationResult, ValidatorError>;
}
}

Method Specifications

kind

Purpose: Identifies the validator type for reporting.

Returns: ValidatorKind enum variant:

  • ValidatorKind::JsonSchema
  • ValidatorKind::Shacl
  • ValidatorKind::Owl

Requirements:

  • Must be a constant value (no I/O)
  • Used in ValidationResult to identify which validator produced each result

validate

Purpose: Validate a payload against model artifacts.

Parameters:

  • artifacts — Resolved model artifacts (may contain schema, SHACL, OWL, etc.)
  • payload — JSON payload to validate

Returns:

  • Ok(ValidationResult) — Validation outcome with violations (if any)
  • Err(ValidatorError) — Validator setup or execution error

Requirements:

  • Must return passed: true only if no violations found
  • Must populate violations with all detected issues
  • Must include path (JSON pointer or similar) for each violation
  • Should skip validation gracefully if required artifact is absent
  • Must not throw exceptions; return structured errors

Behavior when artifact is missing: Validators should return Ok(ValidationResult { passed: true, violations: [] }) and log a warning if the required artifact is absent. The orchestrator in ValidateUseCaseImpl already skips validators when artifacts are unavailable.

ValidationResult Structure

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ValidationResult {
    pub kind:       ValidatorKind,
    pub passed:     bool,
    pub violations: Vec<ValidationViolation>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ValidationViolation {
    pub path:    Option<String>,  // JSON pointer, e.g. "/properties/name"
    pub message: String,
    pub severity: Severity,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum Severity {
    Error,   // Validation failure
    Warning, // Non-blocking issue
    Info,    // Informational
}
}

Validation Orchestration

The core orchestrates validators as follows:

  1. Resolve ArtifactSet for (model, version)
  2. For each configured validator:
    • Skip if required artifact is absent
    • Call validator.validate(artifacts, payload)
    • Collect ValidationResult
  3. Merge results into ValidationReport
  4. Set ValidationReport::passed = true only if all validators pass

Preferred validator: SHACL (richer constraint checking)

Implementation Examples

  • crates/validator-jsonschema — JSON Schema Draft 2020-12
  • crates/validator-shacl — SHACL Turtle validation (preferred)
  • crates/validator-owl — OWL ontology validation (optional)

EnricherPort (Enricher Contract)

Trait Definition

#![allow(unused)]
fn main() {
// crates/core/src/ports/outbound/enricher.rs
#[async_trait::async_trait]
pub trait EnricherPort: Send + Sync {
    async fn enrich(
        &self,
        ctx:    &SecurityContext,
        record: &Record,
    ) -> Result<serde_json::Value, EnricherError>;
}
}

Method Specifications

enrich

Purpose: Augment a record with additional data from external sources.

Parameters:

  • ctx — Security context (for authorization and token passthrough)
  • record — The record to enrich

Returns:

  • Ok(serde_json::Value) — Enriched payload (merged with or replacing original)
  • Err(EnricherError) — Enrichment failed

Requirements:

  • Must be idempotent (same input → same output)
  • May call external APIs (product databases, certification registries, etc.)
  • Should time out gracefully if external service is slow
  • Must forward SecurityContext::raw_token if external service requires it
  • Should log external failures but not crash

Use Case: The EnrichUseCase reads a record, calls the enricher, and writes back the enriched payload.

Error Types

#![allow(unused)]
fn main() {
#[derive(Debug, thiserror::Error)]
pub enum EnricherError {
    #[error("external service unavailable")]
    ServiceUnavailable,
    
    #[error("timeout")]
    Timeout,
    
    #[error("unauthorized")]
    Unauthorized,
    
    #[error("internal: {0}")]
    Internal(String),
}
}

Implementation Notes

  • Enrichers are optional; the core works without them
  • Enrichment is triggered via POST /models/{model}/versions/{version}:enrich
  • Enrichers must support Idempotency-Key to avoid duplicate side effects

ArtifactRegistryPort (Registry Contract)

Trait Definition

#![allow(unused)]
fn main() {
// crates/core/src/ports/outbound/registry.rs
#[async_trait::async_trait]
pub trait ArtifactRegistryPort: Send + Sync {
    async fn resolve(
        &self,
        model: &ModelId,
        ver:   &ModelVersion,
    ) -> Result<ArtifactSet, RegistryError>;

    async fn list_models(&self) -> Result<Vec<ModelDescriptor>, RegistryError>;

    async fn refresh(&self) -> Result<RefreshSummary, RegistryError>;
}
}

Method Specifications

resolve

Purpose: Retrieve all artifacts for a specific model version.

Parameters:

  • model — Model identifier (e.g., product-passport)
  • ver — Version string without leading ‘v’ (e.g., 1.2.0)

Returns:

  • Ok(ArtifactSet) — All available artifacts
  • Err(RegistryError::NotFound) — Model version does not exist
  • Err(RegistryError) — Registry unavailable or invalid

Requirements:

  • Must fetch from the configured URL template
  • Must populate all available artifacts (route, schema, shacl, owl, openapi)
  • Missing optional artifacts should be None, not an error
  • Missing route.json should return NotFound

list_models

Purpose: Return all discovered models.

Returns:

  • Ok(Vec<ModelDescriptor>) — List of {model, version} pairs

Requirements:

  • Must reflect the current in-memory index
  • Used by GET /models endpoint

refresh

Purpose: Re-discover models and atomically swap the index.

Returns:

  • Ok(RefreshSummary) — Summary of refresh operation
  • Err(RegistryError) — Refresh failed

Requirements:

  • Must re-fetch all model artifacts
  • Must build a new index in memory
  • Must atomically swap the index (no downtime)
  • Must return errors per model (not fail entirely if one model fails)

ArtifactSet Structure

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Default)]
pub struct ArtifactSet {
    pub route:      Option<serde_json::Value>,  // required for dispatch
    pub schema:     Option<String>,             // JSON Schema text
    pub shacl:      Option<String>,             // SHACL Turtle text
    pub owl:        Option<String>,             // OWL Turtle text
    pub openapi:    Option<String>,             // OpenAPI YAML/JSON text
}

impl ArtifactSet {
    pub fn is_routable(&self) -> bool {
        self.route.is_some()
    }
}
}

Catalog Entry Format

Catalog entries should provide explicit artifact references per (model, version), for example:

{
  "model": "re-indicators-specification",
  "version": "0.0.3",
  "route_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/route.json",
  "schema_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/schema.json",
  "shacl_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/shacl.ttl"
}

The running service reads catalog entries from one of:

  • REGISTRY_CATALOG_JSON
  • REGISTRY_CATALOG_FILE
  • REGISTRY_CATALOG_URL

Requirements for each artifact reference:

  • it must be a directly fetchable runtime URL to the artifact file
  • it must pass REGISTRY_ALLOWED_HOSTS
  • it must pass REGISTRY_REQUIRE_HTTPS when HTTPS enforcement is enabled

Artifact Reference Defaults

ArtifactFilenameRequired
Route definitionroute_urlOnly for routable operations
JSON Schemaschema_urlNo
SHACL shapesshacl_urlNo
OWL ontologyowl_urlNo
OpenAPI specopenapi_urlNo

Resolution Behavior

  1. On startup or refresh, fetch each explicitly declared artifact reference
  2. Silently skip undeclared or 404 optional artifacts
  3. Mark model as non-routable if no route artifact is present
  4. Cache artifacts only if REGISTRY_CACHE_ENABLED=true (default: disabled)
  5. Refresh index via POST /admin/registry/refresh

Implementation Example

  • crates/registry — catalog-backed artifact registry with URL fetch helper

Security Requirements

All adapters must adhere to these security rules:

  1. Token Passthrough: Forward SecurityContext::raw_token to backend services as Authorization: Bearer <token>
  2. No Token Logging: Never log or persist access tokens
  3. Authorization: Enforce user-level access control where applicable
  4. HTTPS Only: Use HTTPS for all external calls (override with REGISTRY_REQUIRE_HTTPS=false only in dev)
  5. Timeouts: Always set request timeouts to prevent indefinite hangs

Testing Requirements

All adapter implementations must include:

  1. Unit tests — Trait methods with mocked dependencies
  2. Contract tests — Known-good and known-bad inputs
  3. Integration tests — Against real or wiremocked external services
  4. Error handling tests — Network failures, timeouts, malformed responses

Example Test Structure

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_write_success() {
        // Arrange: create adapter and valid record
        // Act: call write()
        // Assert: returns Ok(RecordId)
    }

    #[tokio::test]
    async fn test_write_idempotency_conflict() {
        // Arrange: write once, then retry with different payload
        // Act: call write() with same key
        // Assert: returns Err(StoreError::IdempotencyConflict)
    }

    #[tokio::test]
    async fn test_validate_pass() {
        // Arrange: valid payload and artifacts
        // Act: call validate()
        // Assert: returns Ok(ValidationResult { passed: true, ... })
    }

    #[tokio::test]
    async fn test_validate_fail() {
        // Arrange: invalid payload and artifacts
        // Act: call validate()
        // Assert: returns Ok(ValidationResult { passed: false, violations: [...] })
    }
}
}

Adapter Development Checklist

When implementing a new adapter:

  • Define a new crate in crates/<adapter-name>/
  • Depend on crates/core (ports and domain types only)
  • Implement the appropriate port trait (RecordStorePort, ValidatorPort, etc.)
  • Add unit tests with 100% coverage of trait methods
  • Add contract tests with known-good and known-bad inputs
  • Document adapter-specific configuration in adapter’s README.md
  • Add integration tests (wiremock or testcontainers)
  • Document error handling behavior
  • Add adapter to main Cargo.toml workspace
  • Update deployment guide with adapter setup instructions
  • Add adapter to README.md list of available adapters

Support

For questions about adapter contracts:

  • Review existing implementations in crates/io-memory, crates/validator-jsonschema
  • Open an issue on Codeberg: https://codeberg.org/CE-RISE-software/hex-core-service/issues
  • Contact: ribo@nilu.no

Configuration Reference

All runtime configuration is via environment variables. No config files are required. See .env.example for a ready-to-copy template.


Registry

VariableRequiredDefaultDescription
REGISTRY_MODEYescatalogRegistry backend mode. Current API wiring supports catalog.
REGISTRY_CATALOG_JSONCond.Inline JSON catalog content (string).
REGISTRY_CATALOG_FILECond.Local path to catalog JSON file.
REGISTRY_CATALOG_URLCond.HTTP(S) URL to catalog JSON file.
REGISTRY_ALLOWED_HOSTSYesComma-separated allowed hostnames (e.g. codeberg.org)
REGISTRY_REQUIRE_HTTPSYestrue/false; startup fails if missing or invalid
REGISTRY_CACHE_ENABLEDNofalseEnable artifact caching
REGISTRY_CACHE_TTL_SECSNo300Cache TTL in seconds

Catalog source selection

Exactly one of the following should be set:

  • REGISTRY_CATALOG_JSON
  • REGISTRY_CATALOG_FILE
  • REGISTRY_CATALOG_URL

If none is set, startup fails.

Catalog format

Accepted JSON shapes:

[
  {
    "model": "re-indicators-specification",
    "version": "0.0.3",
    "route_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/route.json",
    "schema_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/schema.json",
    "shacl_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/shacl.ttl"
  }
]

or

{
  "models": [
    {
      "model": "re-indicators-specification",
      "version": "0.0.3",
      "route_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/route.json",
      "schema_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/schema.json"
    }
  ]
}

Rules:

  • Multiple versions for the same model are allowed.
  • Duplicate (model, version) entries are rejected.
  • Catalog entries must declare explicit per-artifact URLs using route_url, schema_url, shacl_url, owl_url, and openapi_url as needed.
  • At least one artifact reference must be declared in each entry.
  • Artifact references must be directly fetchable runtime URLs to the artifact file itself, not repository HTML pages.
  • route_url is only required for routable model operations; validation-only entries may publish only schema_url, shacl_url, or owl_url.
  • If model or version is omitted, the registry attempts to infer them from declared artifact URLs when they match known CE-RISE Codeberg patterns.
  • Every artifact URL is validated against REGISTRY_ALLOWED_HOSTS.
  • Every artifact URL must satisfy REGISTRY_REQUIRE_HTTPS when enabled.

For SHACL behavior and artifact expectations (shacl.ttl), see SHACL Validation.

Refresh behavior

POST /admin/registry/refresh re-loads the catalog source each time:

  • REGISTRY_CATALOG_URL: re-downloads latest JSON from that URL.
  • REGISTRY_CATALOG_FILE: re-reads the file from disk.
  • REGISTRY_CATALOG_JSON: reuses in-memory inline catalog unless changed by process restart or runtime replacement API.

The in-memory index swap is atomic. If the catalog cannot be loaded/parsed, refresh returns an error and the previous index remains active. If individual model entries fail artifact resolution, refresh succeeds with per-entry errors and loads only successful entries.

IO Adapter

VariableRequiredDefaultDescription
IO_ADAPTER_IDYesAdapter identifier (memory or http in current API wiring)
IO_ADAPTER_VERSIONYesAdapter version (e.g. v1)
IO_ADAPTER_BASE_URLCond.Base URL for the HTTP IO Adapter Service
IO_ADAPTER_TIMEOUT_MSNo5000Request timeout in milliseconds

Notes:

  • IO_ADAPTER_ID=memory: in-process memory store (dev/test).
  • IO_ADAPTER_ID=http: enables crates/io-http; requires IO_ADAPTER_BASE_URL.

Auth

VariableRequiredDefaultDescription
AUTH_MODENojwt_jwksAuth provider mode: jwt_jwks, forward_auth, or none
AUTH_JWKS_URLCond.JWKS endpoint URL (AUTH_MODE=jwt_jwks)
AUTH_ISSUERCond.Expected JWT iss (AUTH_MODE=jwt_jwks)
AUTH_AUDIENCECond.Expected JWT aud (AUTH_MODE=jwt_jwks)
AUTH_JWKS_REFRESH_SECSNo3600JWKS key refresh interval seconds (AUTH_MODE=jwt_jwks)
AUTH_FORWARD_SUBJECT_HEADERNox-auth-subjectSubject header name (AUTH_MODE=forward_auth)
AUTH_FORWARD_ROLES_HEADERNox-auth-rolesComma-separated roles header (AUTH_MODE=forward_auth)
AUTH_FORWARD_SCOPES_HEADERNox-auth-scopesSpace-separated scopes header (AUTH_MODE=forward_auth)
AUTH_FORWARD_TENANT_HEADERNoTenant header name (AUTH_MODE=forward_auth)
AUTH_FORWARD_TOKEN_HEADERNoHeader containing raw token to propagate (AUTH_MODE=forward_auth)
AUTH_ALLOW_INSECURE_NONENofalseMust be true to allow AUTH_MODE=none (unsafe, non-production)
AUTH_NONE_SUBJECTNodev-anonymousSubject injected in AUTH_MODE=none
AUTH_NONE_ROLESNoComma-separated roles injected in AUTH_MODE=none
AUTH_NONE_SCOPESNoSpace-separated scopes injected in AUTH_MODE=none
AUTH_NONE_TENANTNoOptional tenant injected in AUTH_MODE=none

Notes:

  • jwt_jwks is for direct bearer JWT validation in this service.
  • forward_auth is for deployments where an upstream proxy/gateway already authenticated the caller and injects identity headers.
  • none is only for isolated dry runs and requires AUTH_ALLOW_INSECURE_NONE=true.
  • Detailed integration guidance: Authentication.

Server

VariableRequiredDefaultDescription
SERVER_HOSTNo0.0.0.0Bind address
SERVER_PORTNo8080Bind port
SERVER_REQUEST_MAX_BYTESNo1048576Max request body size (1 MiB)

Observability

VariableRequiredDefaultDescription
LOG_LEVELNoinfoTracing filter (e.g. debug, info,tower_http=warn)
METRICS_ENABLEDNofalseExpose /admin/metrics (Prometheus format)

OWL Validation Mode

OWL validation is enabled through the hex-validator-owl adapter in API wiring.

  • Runtime mode: embedded profile checks (no external OWL subprocess required).
  • Activation condition: validator executes when owl.ttl is present in resolved artifacts.
  • Missing owl.ttl: validator skips gracefully and returns passed=true with no violations.
  • Invalid owl.ttl: mapped to validator initialization error.
  • Runtime execution fault: mapped to validator execution error.

Operationally this keeps deployment simple (no extra binaries), but the current path is profile-oriented and not a full generic OWL reasoner.

Deployment

This guide covers packaging, containerization, and deployment of the CE-RISE Hex Core Service.

Contents


Local Development

Using Docker Compose

The repository includes a docker-compose.yml for local development with minimal dependencies:

# Copy environment template
cp .env.example .env

# Edit .env with your configuration
# For local development, use the in-memory adapter:
# IO_ADAPTER_ID=memory

# Start the service
docker-compose up

# Or run in the background
docker-compose up -d

# View logs
docker-compose logs -f

# Stop the service
docker-compose down

The compose setup includes:

  • Hex Core Service with io-memory adapter
  • Mock registry (wiremock) for artifact resolution
  • No external dependencies required

Running Natively

For faster development iteration:

# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Build and run
cargo build --release
cargo run -p api

# Or use cargo-watch for auto-reload
cargo install cargo-watch
cargo watch -x 'run -p api'

Container Image

Multi-Stage Dockerfile

The service uses a multi-stage build to produce a minimal runtime image:

Stage 1 — Builder:

  • Base: rust:1-slim
  • Builds the api binary from source
  • Includes all necessary build dependencies

Stage 2 — Runtime:

  • Base: debian:bookworm-slim
  • Contains only the compiled binary and runtime dependencies
  • No source code, no build tools
  • No proprietary adapter binaries

Building Locally

# Build the image
docker build -t hex-core-service:local .

# Run the image
docker run -p 8080:8080 \
  -e REGISTRY_MODE="catalog" \
  -e REGISTRY_CATALOG_URL="https://config.example.org/hex-core/catalog.json" \
  -e IO_ADAPTER_ID="memory" \
  -e AUTH_JWKS_URL="https://keycloak.example.com/realms/cerise/protocol/openid-connect/certs" \
  -e AUTH_ISSUER="https://keycloak.example.com/realms/cerise" \
  -e AUTH_AUDIENCE="hex-core-service" \
  hex-core-service:local

Image Contents

The runtime image includes:

  • /usr/local/bin/hex-core-service — the main binary
  • Minimal dynamic library dependencies (libc, OpenSSL)
  • No proprietary code or adapters
  • Optional: read-only mount point for artifact cache (when REGISTRY_CACHE_ENABLED=true)

Image Size: Approximately 50-80 MB (compressed)


Image Tags and Versioning

Tag Strategy

The CI/CD pipeline produces the following immutable tags:

Tag FormatExamplePurpose
<version>v1.2.0Semantic version from git tag
latestlatestMost recent release on main

latest Tag Policy

Important: The latest tag is not a rolling tag for the main branch.

  • latest tracks the most recent tagged release (e.g., v1.2.0)
  • latest is never published from:
    • Feature branches
    • Pre-release tags (e.g., v1.2.0-rc1)
    • Untagged commits
  • latest always points to a stable, tested release

Tag Examples

After releasing version v1.2.0 from main at commit a1b2c3d, the following tags are pushed:

<registry>/<namespace>/hex-core-service:v1.2.0
<registry>/<namespace>/hex-core-service:latest

For production deployments: Always use explicit version tags (v1.2.0) and treat latest as convenience.


CLI Distribution

Prebuilt hex-cli binaries are published as release assets:

  • https://codeberg.org/CE-RISE-software/hex-core-service/releases

Supported operating systems:

  • Linux
  • macOS
  • Windows

Use these assets when you need CLI access in automation or terminal workflows without building from source.


Environment Configuration

All runtime configuration is via environment variables. See the Configuration Guide for the complete reference.

Minimal Configuration

Required variables for startup:

REGISTRY_MODE=catalog
REGISTRY_CATALOG_URL=https://example.org/catalog.json
IO_ADAPTER_ID=memory
AUTH_JWKS_URL=https://keycloak.example.com/realms/cerise/protocol/openid-connect/certs
AUTH_ISSUER=https://keycloak.example.com/realms/cerise
AUTH_AUDIENCE=hex-core-service

Using ConfigMap and Secrets

Kubernetes example:

apiVersion: v1
kind: ConfigMap
metadata:
  name: hex-core-config
data:
  REGISTRY_MODE: "catalog"
  REGISTRY_CATALOG_URL: "https://config.example.org/hex-core/catalog.json"
  REGISTRY_ALLOWED_HOSTS: "codeberg.org,config.example.org"
  REGISTRY_REQUIRE_HTTPS: "true"
  IO_ADAPTER_ID: "circularise"
  IO_ADAPTER_VERSION: "v1"
  SERVER_PORT: "8080"
  LOG_LEVEL: "info"
  METRICS_ENABLED: "true"

---
apiVersion: v1
kind: Secret
metadata:
  name: hex-core-secrets
type: Opaque
stringData:
  AUTH_JWKS_URL: "https://keycloak.example.com/realms/cerise/protocol/openid-connect/certs"
  AUTH_ISSUER: "https://keycloak.example.com/realms/cerise"
  AUTH_AUDIENCE: "hex-core-service"
  IO_ADAPTER_BASE_URL: "https://io-adapter.internal.example.com"

GitOps Catalog Deployment Pattern

Use a single catalog artifact as the registry source of truth:

{
  "models": [
    {
      "model": "re-indicators-specification",
      "version": "0.0.3",
      "route_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/route.json",
      "schema_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/schema.json",
      "shacl_url": "https://codeberg.org/CE-RISE-models/re-indicators-specification/raw/tag/pages-v0.0.3/generated/shacl.ttl"
    }
  ]
}

Each artifact reference in the catalog must be a directly fetchable runtime URL, and must satisfy:

  • REGISTRY_ALLOWED_HOSTS
  • REGISTRY_REQUIRE_HTTPS

Recommended flow:

  1. Update catalog.json via GitOps pull request.
  2. Publish catalog to stable URL (or mount as file in cluster).
  3. Trigger POST /admin/registry/refresh.
  4. Confirm GET /models reflects the new catalog.

No service restart is required for model list changes.


Container Registry

Target Registry

Primary: Scaleway Container Registry

  • Registry: rg.fr-par.scw.cloud
  • Namespace: ce-rise
  • Repository: hex-core-service

Full Image Path:

rg.fr-par.scw.cloud/ce-rise/hex-core-service:<tag>

Authentication

# Login to Scaleway Container Registry
docker login rg.fr-par.scw.cloud -u <username> -p <secret-key>

# Pull an image
docker pull rg.fr-par.scw.cloud/ce-rise/hex-core-service:v1.2.0

For Kubernetes, create an image pull secret:

kubectl create secret docker-registry scaleway-registry \
  --docker-server=rg.fr-par.scw.cloud \
  --docker-username=<username> \
  --docker-password=<secret-key>

Image Retention

  • Version tags (v1.2.0): Retained indefinitely
  • latest tag: Always points to the most recent release

Kubernetes Deployment

Deployment Manifest

Example deployment with all recommended practices:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hex-core-service
  namespace: cerise
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hex-core-service
  template:
    metadata:
      labels:
        app: hex-core-service
        version: v1.2.0
    spec:
      imagePullSecrets:
        - name: scaleway-registry
      containers:
        - name: hex-core-service
          image: rg.fr-par.scw.cloud/ce-rise/hex-core-service:v1.2.0
          ports:
            - containerPort: 8080
              name: http
          envFrom:
            - configMapRef:
                name: hex-core-config
            - secretRef:
                name: hex-core-secrets
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi
          livenessProbe:
            httpGet:
              path: /admin/health
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /admin/ready
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop:
                - ALL

Service Manifest

apiVersion: v1
kind: Service
metadata:
  name: hex-core-service
  namespace: cerise
spec:
  selector:
    app: hex-core-service
  ports:
    - name: http
      port: 80
      targetPort: 8080
  type: ClusterIP

Ingress (Optional)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hex-core-service
  namespace: cerise
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts:
        - api.cerise.example.com
      secretName: hex-core-tls
  rules:
    - host: api.cerise.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: hex-core-service
                port:
                  number: 80

Deployment Steps

# Apply all manifests
kubectl apply -f deploy/k8s/configmap.yaml
kubectl apply -f deploy/k8s/secret.yaml
kubectl apply -f deploy/k8s/deployment.yaml
kubectl apply -f deploy/k8s/service.yaml

# Check rollout status
kubectl rollout status deployment/hex-core-service -n cerise

# Verify pods are ready
kubectl get pods -n cerise -l app=hex-core-service

# Check logs
kubectl logs -f deployment/hex-core-service -n cerise

# Test health endpoint
kubectl port-forward -n cerise svc/hex-core-service 8080:80
curl http://localhost:8080/admin/health

Release Process

Automated Release Pipeline

Releases are fully automated via CI/CD. To create a new release:

  1. Ensure all tests pass on main branch
  2. Tag the commit with semantic version:
    git tag -a v1.2.0 -m "Release v1.2.0"
    git push origin v1.2.0
    
  3. CI/CD automatically:
    • Runs full test suite (including integration tests)
    • Builds release binary (cargo build --release)
    • Builds hex-cli archives for Linux/macOS/Windows and uploads them as release-pipeline artifacts
    • Builds and pushes Docker image with vX.Y.Z tag
    • Promotes the same image to latest
    • Publishes hex-cli binaries as release artifacts (primary distribution channel)
    • Optionally forwards release tags to SDK repositories (Go/TypeScript/Python)

CLI Binary Availability

hex-cli binaries are produced by the release workflow and published as release-run artifacts with these archive names:

  • hex-cli-<version>-linux-x86_64.tar.gz
  • hex-cli-<version>-macos-x86_64.tar.gz
  • hex-cli-<version>-windows-x86_64.tar.gz

Supported platform matrix:

OSRust targetCPU architectureArchive suffix
Linuxx86_64-unknown-linux-muslx86_64 (amd64)linux-x86_64
macOSx86_64-apple-darwinx86_64 (Intel)macos-x86_64
Windowsx86_64-pc-windows-gnux86_64 (amd64)windows-x86_64

OpenAPI Spec Release Model

OpenAPI specs are released and persisted in-repo via git history and tags (not as separate OpenAPI artifacts):

  • Source of truth:
    • crates/api/src/openapi.json
    • crates/io-http/src/io_adapter_openapi.json
  • Versioning:
    • semantic git tags (vX.Y.Z) identify the released spec version.
  • Commit-time CI:
    • OpenAPI spec validation runs in Rust tests (cargo test) in CI.
    • No separate OpenAPI workflow or artifact export is required.

Release Checklist

Before tagging a release:

  • All CI checks pass on main
  • CHANGELOG.md updated with release notes
  • CITATION.cff version and date updated
  • Documentation reflects new features/changes
  • Breaking changes are clearly documented
  • Migration guide provided (if applicable)

CLI Distribution Policy

Current agreed policy:

  • CLI binaries are attached directly to each tagged release on the Codeberg release page.
  • No Homebrew/Scoop/crates.io publication is required for normal releases.
  • Users should download the OS/CPU-specific archive listed in CLI Binary Availability.

Optional SDK Generation and Publication Toggles

SDK generation and publishing are disabled by default. Enable explicitly in CI variables/secrets:

  • SDK_GENERATION_ENABLED=true
    • Generates TypeScript (typescript-fetch), Python, and Go SDKs from API OpenAPI.
    • Uploads generated SDKs as workflow artifacts.
  • SDK_PUBLISH_NPM_ENABLED=true
    • Publishes TypeScript SDK to npm.
    • Requires NPM_TOKEN secret.
  • SDK_PUBLISH_PYPI_ENABLED=true
    • Builds and publishes Python SDK to PyPI.
    • Requires PYPI_API_TOKEN secret.
  • SDK_PUBLISH_GO_ENABLED=true
    • Pushes generated Go SDK to dedicated repository and tags with release version.
    • Requires:
      • GO_SDK_REPO variable (owner/repo)
      • GO_SDK_REPO_TOKEN secret
      • Optional GO_SDK_BRANCH variable (default main)

Cross-Forge Mirroring

Source of Truth: Codeberg (https://codeberg.org/CE-RISE-software/hex-core-service)

Mirror: GitHub (https://github.com/CE-RISE-software/hex-core-service)

The GitHub mirror is read-only and used for:

  • Release archival
  • Zenodo DOI integration
  • Broader discoverability

Mirror Pipeline:

EventAction
Tag v*.*.* pushed on CodebergMirror sync propagates the tag to GitHub
Tag arrives on GitHubGitHub Actions creates a Release automatically
GitHub Release publishedZenodo archives snapshot and mints DOI
Mirror failureAlert logged; does not fail Codeberg pipeline

Rollback Procedure

If a release has critical issues:

# Kubernetes rollback to previous revision
kubectl rollout undo deployment/hex-core-service -n cerise

# Or rollback to a specific version
kubectl set image deployment/hex-core-service -n cerise \
  hex-core-service=rg.fr-par.scw.cloud/ce-rise/hex-core-service:v1.1.0

# Verify rollback
kubectl rollout status deployment/hex-core-service -n cerise

Note: Git tags are never deleted. If a release is critically flawed, tag a new patch version with fixes.


Production Readiness Checklist

Before deploying to production:

  • All required environment variables configured
  • Secrets stored in Kubernetes Secrets (never in ConfigMaps)
  • Resource requests and limits set appropriately
  • Health and readiness probes configured
  • Logging level set to info or warn
  • Metrics endpoint enabled (METRICS_ENABLED=true)
  • Prometheus scraping configured
  • Alert rules defined and tested
  • Network policies restrict egress to required services
  • JWKS URL is reachable from the cluster
  • IO adapter service is accessible
  • Registry URLs are accessible (or allowlist configured)
  • Backup and disaster recovery plan documented
  • Runbook reviewed by operations team

Operations Runbook

This runbook provides operational procedures for running and maintaining the CE-RISE Hex Core Service in production.

Contents


Health and Readiness Checks

Liveness Probe

Endpoint: GET /admin/health

Purpose: Determines if the service process is alive and responding to requests.

Expected Response:

{
  "status": "ok"
}

HTTP Status: 200 OK

Kubernetes Configuration:

livenessProbe:
  httpGet:
    path: /admin/health
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10
  timeoutSeconds: 3
  failureThreshold: 3

Readiness Probe

Endpoint: GET /admin/ready

Purpose: Determines if the service is ready to accept traffic. Returns success only when:

  • The artifact registry has been loaded successfully
  • At least one model is available
  • All required adapters are initialized

Expected Response (ready):

{
  "status": "ready",
  "registry_loaded": true,
  "models_available": 5
}

HTTP Status: 200 OK (ready) or 503 Service Unavailable (not ready)

Kubernetes Configuration:

readinessProbe:
  httpGet:
    path: /admin/ready
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
  timeoutSeconds: 3
  failureThreshold: 3

Status Endpoint

Endpoint: GET /admin/status

Purpose: Returns detailed runtime status information.

Expected Response:

{
  "uptime_seconds": 3600,
  "registry": {
    "models_loaded": 5,
    "last_refresh": "2024-01-15T10:30:00Z",
    "cache_enabled": false
  },
  "config": {
    "io_adapter_id": "circularise",
    "io_adapter_version": "v1",
    "validators_enabled": ["shacl", "jsonschema"]
  }
}

Version and Model Count Quick Check

Use these lightweight probes during rollout validation:

curl http://localhost:8080/admin/version
curl http://localhost:8080/admin/models/count

Expected:

  • /admin/version returns both service_version and openapi_version.
  • /admin/models/count returns a non-negative integer.
  • models_count = 0 is valid when registry is empty/not loaded yet.

Registry Refresh Flow

The artifact registry can be manually refreshed to discover new models or updated versions without restarting the service.

Manual Refresh

Endpoint: POST /admin/registry/refresh

Authentication: Requires admin token or network-level protection (mTLS, private subnet).

Process:

  1. The registry re-loads the configured catalog source (REGISTRY_CATALOG_URL or REGISTRY_CATALOG_FILE).
  2. The registry resolves the explicitly declared artifact references for each catalog entry.
  3. A new index is built in memory.
  4. The index is atomically swapped (no downtime).
  5. A refresh summary is returned.

If REGISTRY_CATALOG_JSON is used, refresh reuses the inline catalog value loaded at startup.

Expected response shape:

{
  "refreshed_at": "2026-03-03T18:12:45Z",
  "models_found": 5,
  "errors": []
}

Partial failure example:

{
  "refreshed_at": "2026-03-03T18:12:45Z",
  "models_found": 4,
  "errors": [
    "product-passport@2.0.0: model not found in registry: product-passport v2.0.0 (https://...)"
  ]
}

Behavior details:

  • Catalog load/parse failure: refresh returns error and previous index remains active.
  • Entry-level fetch/validation failure: refresh succeeds, failing entries are excluded, and errors are listed.
  • Duplicate (model, version) in catalog: refresh fails and previous index remains active.

GitOps workflow

For GitOps-managed deployments:

  1. Publish/update catalog.json in your config repo/object storage.
  2. Ensure service points to it via REGISTRY_CATALOG_URL or mounted REGISTRY_CATALOG_FILE.
  3. Trigger POST /admin/registry/refresh.
  4. Verify with GET /models.

This allows model additions/removals/version updates without restarting the service.

When to Refresh

  • After deploying new model versions to the registry
  • When models are returning 404 Not Found errors
  • As part of a scheduled maintenance window
  • When the /admin/status endpoint shows stale registry data

Automatic Refresh

Automatic refresh is not implemented by default. If needed, implement it as:

  • A Kubernetes CronJob calling the refresh endpoint
  • An external scheduler (cron, Airflow, etc.)
  • A sidecar container with a polling loop

Recommended Interval: Every 5-15 minutes, depending on how frequently models are updated.


CLI Operational Use

For operator workflows, use the prebuilt hex-cli release assets:

  • https://codeberg.org/CE-RISE-software/hex-core-service/releases

This is useful for scripted checks and admin tasks in environments where local Rust toolchains are not installed.


Authentication Operations

For full authentication architecture and integration patterns, see Authentication.

Runtime Auth Mode Check

Confirm active auth mode in environment:

echo "$AUTH_MODE"

Expected values:

  • jwt_jwks
  • forward_auth
  • none (isolated non-production only)

Quick Validation Path Checks

  • jwt_jwks: verify AUTH_JWKS_URL, AUTH_ISSUER, AUTH_AUDIENCE are set and reachable.
  • forward_auth: verify gateway injects configured subject/roles/scopes headers.
  • none: verify AUTH_ALLOW_INSECURE_NONE=true is explicitly set.

SHACL Validation Operations

Use this section together with SHACL Validation, which defines current validation scope and limits.

Preconditions

  • The model/version is present in the active registry index.
  • The model artifact folder contains shacl.ttl.
  • Registry refresh has been executed after catalog/artifact updates.

Quick Check

  1. Confirm model exists:
    curl http://localhost:8080/models
    
  2. Confirm SHACL artifact is resolvable:
    curl http://localhost:8080/models/{model}/versions/{version}/shacl
    
  3. Validate payload:
    curl -X POST http://localhost:8080/models/{model}/versions/{version}:validate \
      -H "Authorization: Bearer <token>" \
      -H "Content-Type: application/json" \
      -d '{"payload":{...}}'
    

Interpreting Results

  • passed=true: all executed validators passed.
  • results[].kind="shacl": SHACL adapter result block.
  • results[].violations[]: path/message/severity entries for SHACL failures.

Common SHACL Failure Patterns

  • Invalid enum values (for example record_scope or relation_type)
  • Invalid timestamp format (non-RFC3339)
  • Wrong primitive type (integer/number mismatch)
  • Unexpected keys inside closed sections such as applied_schemas[*]

OWL Validation Operations

Runtime Mode

  • OWL validation currently runs in embedded profile mode.
  • No external reasoner subprocess is required by default deployment.

Preconditions

  • The model/version is present in registry index.
  • The model artifact folder contains owl.ttl.
  • Registry refresh has been executed after artifact/catalog updates.

Quick Check

  1. Confirm OWL artifact resolves:
    curl http://localhost:8080/models/{model}/versions/{version}/owl
    
  2. Submit payload to validate endpoint:
    curl -X POST http://localhost:8080/models/{model}/versions/{version}:validate \
      -H "Authorization: Bearer <token>" \
      -H "Content-Type: application/json" \
      -d '{"payload":{...}}'
    
  3. Confirm response contains OWL result block:
    • results[].kind == "Owl"

Error Mapping

  • Missing owl.ttl: validator is skipped.
  • Invalid ontology artifact: validator initialization error.
  • Runtime failure in validator execution path: validator execution error.

Performance Notes

  • OWL artifacts can be larger than route/schema artifacts; refresh and in-memory footprint grow accordingly.
  • Keep OWL artifacts versioned and immutable where possible to avoid cache churn.
  • If OWL validation latency grows, monitor validation_duration_seconds{validator="owl"} and scale replicas before increasing request concurrency.

Metrics Reference

Endpoint: GET /admin/metrics (when METRICS_ENABLED=true)

Format: Prometheus text exposition format

Key Metrics

Request Metrics

  • http_requests_total{method, path, status} — Total HTTP requests
  • http_request_duration_seconds{method, path} — Request latency histogram
  • http_requests_in_flight{method, path} — Current concurrent requests

Validation Metrics

  • validation_requests_total{model, version, result} — Total validation requests (result: pass/fail)
  • validation_duration_seconds{model, version, validator} — Validation latency per validator
  • validation_violations_total{model, version, severity} — Violation counts by severity

Registry Metrics

  • registry_models_loaded — Number of models currently loaded
  • registry_refresh_total{result} — Total refresh attempts (result: success/failure)
  • registry_refresh_duration_seconds — Refresh operation latency
  • registry_artifact_fetch_total{model, version, result} — Artifact fetch attempts

IO Adapter Metrics

  • io_adapter_requests_total{operation, result} — Outbound IO adapter calls
  • io_adapter_request_duration_seconds{operation} — IO adapter latency
  • io_adapter_errors_total{operation, error_type} — IO adapter errors

Idempotency Metrics

  • idempotency_conflicts_total{model, version} — Idempotency key conflicts

Alerting Rules

Recommended Prometheus alert rules:

groups:
  - name: hex-core-service
    rules:
      - alert: HighErrorRate
        expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High HTTP 5xx error rate"

      - alert: RegistryRefreshFailed
        expr: registry_refresh_total{result="failure"} > 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Registry refresh failed"

      - alert: IOAdapterDown
        expr: rate(io_adapter_errors_total[5m]) > 0.1
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "High IO adapter error rate"

      - alert: ServiceNotReady
        expr: up{job="hex-core-service"} == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Service is not ready"

Troubleshooting Guide

API Error Matrix

HTTPTypical error bodyLikely causeUser action
401auth error responseMissing/invalid token, wrong auth mode config, expired tokenProvide valid token, verify auth variables and mode
403auth error responseCaller authenticated but denied by policy/scope/roleRequest required role/scope or adjust policy
404MODEL_NOT_FOUNDModel/version not in active registry indexCheck catalog entry, trigger /admin/registry/refresh, retry
409IDEMPOTENCY_CONFLICTReused Idempotency-Key with different payloadUse a new idempotency key for changed payload
422VALIDATION_FAILED or NOT_ROUTABLEPayload does not satisfy model artifacts or route constraintsInspect violations, fix payload, retry
500VALIDATOR_ERROR or INTERNAL_ERRORServer-side validator/runtime issueCheck logs and validator artifacts
502STORE_ERROR or REGISTRY_ERRORDownstream IO adapter/registry fetch failureCheck downstream service/network health and retry

Service Won’t Start

Symptoms: Container exits immediately or crashes in a loop.

Diagnosis:

  1. Check logs for configuration errors:
    kubectl logs -f deployment/hex-core-service
    
  2. Verify all required environment variables are set (see configuration.md)
  3. Check JWKS URL is reachable:
    curl -v $AUTH_JWKS_URL
    
  4. Verify registry URL template is valid

Common Causes:

  • Missing catalog source (REGISTRY_CATALOG_URL, REGISTRY_CATALOG_FILE, or REGISTRY_CATALOG_JSON)
  • Invalid AUTH_JWKS_URL (unreachable or malformed)
  • Missing IO_ADAPTER_BASE_URL when using HTTP adapter
  • Network policy blocking outbound registry access

Registry Not Loading Models

Symptoms: /admin/ready returns 503, /models returns empty list.

Diagnosis:

  1. Check registry refresh endpoint:
    curl -X POST http://localhost:8080/admin/registry/refresh
    
  2. Check logs for registry errors
  3. Verify registry URLs are accessible from the pod:
    kubectl exec -it deployment/hex-core-service -- wget -O- $REGISTRY_CATALOG_URL
    

Common Causes:

  • Catalog URL/file path is wrong or unreachable
  • REGISTRY_ALLOWED_HOSTS blocks the registry domain
  • REGISTRY_REQUIRE_HTTPS=true but registry uses HTTP
  • No routable artifact references are published for the affected models

Validation Always Fails

Symptoms: All validation requests return passed: false.

Diagnosis:

  1. Test with a known-good payload (check model documentation)
  2. Verify artifact contents:
    curl http://localhost:8080/models/{model}/versions/{version}/schema
    curl http://localhost:8080/models/{model}/versions/{version}/shacl
    
  3. Check validator logs for parsing errors
  4. Ensure payload matches the model’s expected structure

Common Causes:

  • Malformed artifact (invalid JSON Schema or SHACL Turtle)
  • Payload is for a different model version
  • Validator library incompatibility (check crate versions)

Authentication Errors

Symptoms: All requests return 401 Unauthorized or 403 Forbidden.

Diagnosis:

  1. Verify JWT is valid:
    curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/models
    
  2. Decode JWT and check claims (use jwt.io)
  3. Verify AUTH_ISSUER matches token’s iss claim
  4. Verify AUTH_AUDIENCE matches token’s aud claim
  5. Check JWKS is being fetched successfully (logs)

Common Causes:

  • Token expired (exp claim in the past)
  • Token not yet valid (nbf claim in the future)
  • iss or aud mismatch
  • JWKS cache stale (wait for AUTH_JWKS_REFRESH_SECS or restart)
  • Token signed with a key not in JWKS

IO Adapter Timeout

Symptoms: Requests to create/query endpoints time out or return 504 Gateway Timeout.

Diagnosis:

  1. Check IO adapter service health directly
  2. Review IO_ADAPTER_TIMEOUT_MS setting
  3. Check network latency between core and adapter
  4. Review IO adapter logs for slow queries

Resolution:

  • Increase IO_ADAPTER_TIMEOUT_MS if adapter legitimately needs more time
  • Scale IO adapter service if it’s overloaded
  • Check for network issues (firewalls, DNS resolution)
  • Review slow queries with the IO adapter service team

Idempotency Conflicts

Symptoms: Requests return 409 Conflict with “idempotency conflict” error.

Diagnosis:

  1. Verify the Idempotency-Key is unique per logical operation
  2. Check if a previous request with the same key succeeded
  3. Review IO adapter’s idempotency implementation

Expected Behavior:

  • Same key + same payload → same result (replay protection)
  • Same key + different payload → 409 Conflict (error)

Resolution:

  • If the operation already succeeded, the client should accept the conflict
  • If the operation failed, use a new Idempotency-Key

Common Issues

High Memory Usage

Symptoms: OOM kills, high memory metrics.

Possible Causes:

  • Large number of models loaded in registry (index is in-memory)
  • Large artifact files (especially OpenAPI or OWL)
  • Memory leak in a validator or adapter

Mitigation:

  • Increase memory limits in deployment
  • Disable unused validators
  • Monitor for leaks with profiling tools
  • Consider implementing artifact streaming for large files

Stale Artifact Cache

Symptoms: Service serves old model versions after registry update.

Diagnosis:

  1. Check if REGISTRY_CACHE_ENABLED=true
  2. Verify REGISTRY_CACHE_TTL_SECS is appropriate

Resolution:

  • Manually refresh: POST /admin/registry/refresh
  • Lower REGISTRY_CACHE_TTL_SECS
  • Disable cache entirely if models update frequently

Log Volume Too High

Symptoms: High disk usage from logs, log aggregation costs.

Mitigation:

  • Adjust LOG_LEVEL to info or warn (avoid debug in production)
  • Configure log sampling in high-traffic environments
  • Ensure Authorization headers are redacted (should be automatic)
  • Use structured logging to enable efficient filtering

Emergency Procedures

Rollback Procedure

If a deployment causes issues:

  1. Roll back to previous image tag:

    kubectl set image deployment/hex-core-service \
      hex-core-service=<registry>/<namespace>/hex-core-service:<previous-version>
    
  2. Verify rollback:

    kubectl rollout status deployment/hex-core-service
    curl http://localhost:8080/admin/health
    

Cache Clear (if implemented)

Endpoint: POST /admin/cache/clear

Clears only the artifact cache, not business data. Use when:

  • Artifacts are served stale despite refresh
  • Suspected cache corruption

Note: This endpoint is optional and may not be implemented in all deployments.