Contact us to get provisioned, commit your CALM document and config mapping, and ArchRails will enforce your architecture on every PR or MR automatically.
Contact us to get provisioned — we'll set up your organization, provide credentials, and connect your GitHub or GitLab integration. Choose your platform below for setup details.
We install the ArchRails GitHub App into your organization during provisioning. It listens for pull request events and posts CALM-backed review comments — no CI changes needed on your side.
The app requests read access to pull request diffs and write access to post review comments. Your source code is never copied or stored.
GitLab integration uses a CI pipeline job that fires on every merge request event and calls the ArchRails webhook. No marketplace install needed — just add the job and set three variables.
You'll receive your ARCHRAILS_ENDPOINT, GITLAB_WEBHOOK_SECRET, and ARCHRAILS_TENANT_ID during provisioning. Contact us to get started.
Add this job to your .gitlab-ci.yml
stages:
- review
archrails_review:
stage: review
image: alpine:3.20
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
before_script:
- apk add --no-cache curl
script:
- echo "Triggering ArchRails review..."
- |
set -euo pipefail
: "${ARCHRAILS_ENDPOINT:?ARCHRAILS_ENDPOINT is required}"
: "${GITLAB_WEBHOOK_SECRET:?GITLAB_WEBHOOK_SECRET is required}"
: "${ARCHRAILS_TENANT_ID:?ARCHRAILS_TENANT_ID is required}"
delivery_id="${CI_PIPELINE_ID}-${CI_JOB_ID}-${CI_COMMIT_SHA}"
curl --silent --show-error --fail-with-body -X POST "${ARCHRAILS_ENDPOINT}" \
-H "X-Gitlab-Token: ${GITLAB_WEBHOOK_SECRET}" \
-H "X-Gitlab-Event: Merge Request Hook" \
-H "X-Gitlab-Delivery: ${delivery_id}" \
-H "Content-Type: application/json" \
-d "{
\"object_kind\": \"merge_request\",
\"archrails_tenant_id\": \"${ARCHRAILS_TENANT_ID}\",
\"project\": {
\"id\": ${CI_PROJECT_ID},
\"path_with_namespace\": \"${CI_PROJECT_PATH}\",
\"web_url\": \"${CI_PROJECT_URL}\"
},
\"object_attributes\": {
\"iid\": ${CI_MERGE_REQUEST_IID},
\"action\": \"update\",
\"last_commit\": { \"id\": \"${CI_COMMIT_SHA}\" },
\"url\": \"${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}\"
}
}"
Set these CI/CD variables in your project settings
| Variable | Where to get it |
|---|---|
| ARCHRAILS_ENDPOINT | Provided by the ArchRails team on provisioning |
| GITLAB_WEBHOOK_SECRET | Shared secret provided by the ArchRails team on provisioning |
| ARCHRAILS_TENANT_ID | Your organization identifier, provided on provisioning |
Add two files to your repo: a calm.json describing your architecture graph,
and a .archrails/config.yaml that maps your source paths to CALM node IDs.
Together these become the enforceable source of truth ArchRails verifies every PR against.
CALM document: docs/architecture/calm.json
Config mapping: .archrails/config.yaml
version: 1
default_architecture: layered-services
governance:
provider: calm
file: docs/architecture/calm.json # path to your CALM document
mapping:
- path: src/EcommercePlatform.java
node: system-ecommerce-platform
- path: src/EcommercePlatform.java#ApiGateway
node: service-api-gateway
- path: src/EcommercePlatform.java#OrderService
node: service-order
- path: src/EcommercePlatform.java#InventoryService
node: service-inventory
- path: src/EcommercePlatform.java#PaymentService
node: service-payment
- path: src/EcommercePlatform.java#OrderDatabase
node: db-orders
- path: src/EcommercePlatform.java#InventoryDatabase
node: db-inventory
review:
on: pull_request
integrations:
github:
app: archrails
{
"$schema": "https://calm.finos.org/release/1.2/meta/calm.json",
"metadata": {
"owner": "platform-team@example.com",
"version": "1.0.0",
"description": "E-commerce order processing platform"
},
"nodes": [
{ "unique-id": "service-api-gateway", "node-type": "service", "name": "API Gateway",
"interfaces": [{ "unique-id": "iface-api-http", "protocol": "HTTPS", "port": 443 }] },
{ "unique-id": "service-order", "node-type": "service", "name": "Order Service",
"interfaces": [{ "unique-id": "iface-order-rest", "protocol": "HTTPS", "port": 8080 }] },
{ "unique-id": "service-payment", "node-type": "service", "name": "Payment Service",
"interfaces": [{ "unique-id": "iface-payment-rest", "protocol": "HTTPS", "port": 8080 }] },
{ "unique-id": "db-orders", "node-type": "database", "name": "Order Database",
"interfaces": [{ "unique-id": "iface-orders-sql", "protocol": "JDBC", "port": 5432 }] }
],
"relationships": [
{
"unique-id": "rel-connects-api-order",
"relationship-type": {
"connects": {
"source": { "node": "service-api-gateway", "interfaces": ["iface-api-http"] },
"destination": { "node": "service-order", "interfaces": ["iface-order-rest"] }
}
}
},
{
"unique-id": "rel-connects-order-payment",
"relationship-type": {
"connects": {
"source": { "node": "service-order", "interfaces": ["iface-order-rest"] },
"destination": { "node": "service-payment", "interfaces": ["iface-payment-rest"] }
}
}
},
{
"unique-id": "rel-connects-order-db",
"relationship-type": {
"connects": {
"source": { "node": "service-order", "interfaces": ["iface-order-rest"] },
"destination": { "node": "db-orders", "interfaces": ["iface-orders-sql"] }
}
}
}
]
}
ArchRails uses the mapping section in your config to determine which CALM nodes are touched by each PR diff.
Only the relevant nodes and their relationships are checked — changes to services/payment/ won't trigger Order Service rules.
mapping:
# Order service module → CALM node
- path: services/order/src
node: service-order
- path: services/order/src/db
node: db-orders
# Inventory service module → CALM node
- path: services/inventory/src
node: service-inventory
# Payment service module → CALM node
- path: services/payment/src
node: service-payment
# API Gateway module → CALM node
- path: services/api-gateway/src
node: service-api-gateway
scopes:
- name: "Order + Inventory"
match:
any_prefix:
- "services/order/"
- "services/inventory/"
- name: "Payment"
match:
any_prefix: ["services/payment/"]
- name: "Gateway"
match:
any_prefix: ["services/api-gateway/"]
For each PR, ArchRails computes the set of changed file paths, resolves them to CALM node IDs via the mapping table, then verifies only the relationships and interface contracts involving those nodes. No other nodes or constraints are evaluated.
On every PR or MR, ArchRails posts a review comment citing the exact CALM node ID, relationship, or interface contract that was violated — with a plain-English explanation of why. No generic guesses.
🏗️ CALM Architecture Review
⚠️ Relationship violation: OrderService calls PaymentService directly.
CALM node rel-connects-order-payment requires routing via service-api-gateway.
→ calm.json · rel-connects-order-payment
⚠️ Interface breach: OrderDatabase accessed via HTTP.
Node db-orders declares JDBC:5432 only.
→ calm.json · db-orders · iface-orders-sql
✅ Node boundary respected: InventoryService changes are scoped to
service-inventory — no cross-node leakage detected.
Sources: .archrails/config.yaml · calm.json · service-order · db-orders
calm.json with additional relationships and interface contracts as your system grows
config.yaml as new services or modules are introduced
A few directions to get more value from ArchRails.