Lesson 5 of 6 · 7 min read · Governance

Controls

Controls are how you attach compliance and security requirements to your architecture — "this database must encrypt at rest", "this relationship must use mTLS", "this service must emit audit logs." ArchRails enforces two flavors: CALM controls (you author the requirement, ArchRails validates your config matches the schema) and AR-CTRL controls (built-in architectural checks that fire on PRs without per-tenant configuration).

In this lesson
  1. CALM controls vs. AR-CTRL controls
  2. Anatomy of a CALM control
  3. Attaching to nodes and relationships
  4. The AR-CTRL catalog
  5. Evidence and config URLs
  6. Worked example · encrypt PII at rest
  7. Using the audit trail in your audit
  8. Common mistakes
Don’t want to author from scratch? Browse the control template catalog — paste-in CALM snippets for PCI DSS v4, GDPR Art. 32, SOC 2, HIPAA, NIST CSF 2.0, ISO 27001:2022, DORA, MiFID II, NYDFS Part 500, and SOX 404 ITGC. Each carries the requirement schemas and config shapes the engine evaluates. Public Beta.

CALM controls vs. AR-CTRL controls

Two distinct things travel under the word "control" in ArchRails. Knowing which is which avoids hours of confused debugging.

CALM controls     Authored by you, on a node / relationship / flow.
                  Each requirement points at a JSON Schema (the rule) and
                  a config document (your implementation). Validator
                  fetches both and runs JSON Schema validation.
                  Failure → AR-CTRL-001.

AR-CTRL controls  Built-in architectural checks that ship with ArchRails.
                  No per-tenant config; fire on PRs based on the changed
                  files + graph context. Examples: AR-CTRL-AUTHZ-001
                  (auth removed from governed code), AR-CTRL-DB-BOUNDARY-001
                  (service talks to a DB it isn't permitted to).

CALM controls are for your rules (your compliance team's requirements, your security org's standards). AR-CTRL controls are for universal rules that apply to every CALM architecture — you don't author them, you just benefit from them.

Anatomy of a CALM control

A control lives under the controls key of any node, relationship, or flow. The shape is:

"controls": {
  // One key per control "domain" — e.g. encryption, tls, auth, retention
  "encryption": {
    "description": "Data must be encrypted at rest with AES-256 or stronger.",
    "requirements": [
      {
        "control-requirement-url": "https://controls.myorg.io/encryption-at-rest.schema.json",
        "control-config-url":      "https://controls.myorg.io/orders-db/encryption.config.json"
      }
    ]
  }
}

Three things to know:

Attaching to nodes and relationships

On a node:

{
  "unique-id": "orders-db",
  "node-type": "database",
  "name":      "Orders Database",
  "description": "Holds order ledger. PII inside (customer email, billing address).",
  "data-classification": "PII",
  "controls": {
    "encryption": {
      "description": "At-rest encryption required for PII stores.",
      "requirements": [
        {
          "control-requirement-url": "https://controls.myorg.io/encryption-at-rest.schema.json",
          "control-config-url":      "https://controls.myorg.io/orders-db/encryption.config.json"
        }
      ]
    }
  }
}

On a relationship:

{
  "unique-id": "orders-server-to-orders-db",
  "relationship-type": {
    "connects": {
      "source":      { "node": "orders-server" },
      "destination": { "node": "orders-db" }
    }
  },
  "protocol": "tls",
  "controls": {
    "tls": {
      "description": "All connections to PII stores must use TLS 1.3.",
      "requirements": [
        {
          "control-requirement-url": "https://controls.myorg.io/tls-1-3.schema.json",
          "control-config-url":      "https://controls.myorg.io/orders-edge/tls.config.json"
        }
      ]
    }
  }
}
Where you attach matters for enforcement. A control on a node applies to the node as a whole ("this database must be encrypted"). A control on a relationship applies to that specific edge ("this connection must use TLS"). Controls on flows apply across the whole transition sequence. Attach as narrowly as possible — broad controls produce noisy false positives.

The AR-CTRL catalog

A non-exhaustive sample of what ships built-in. These fire on PRs without any per-tenant configuration — they're activated by the architecture shape itself:

AR-CTRL-001  CALM_CONTROL_REQUIREMENT_VIOLATION   Your config doesn't satisfy the schema.
AR-CTRL-002  CALM_CONTROL_FETCH_ERROR             A control URL couldn't be fetched.
AR-CTRL-003  CALM_CONTROL_MISSING_URLS            Requirement entry is missing URLs.

AR-CTRL-AUTHZ-001             Authorization guard removed from governed code.
AR-CTRL-DB-BOUNDARY-001       Service called a DB it isn't permitted to talk to.
AR-CTRL-PUBLIC-EXPOSURE-001   Internal node became externally reachable.
AR-CTRL-SECRET-001            Secret-shaped string committed to governed file.
AR-CTRL-SENSITIVE-FIXTURE-001 Real-looking PII/PHI/PCI in a test fixture.
AR-CTRL-SENSITIVE-LOG-001     Sensitive value emitted to a logger.

Reviewers see the rule ID and a one-sentence message on the PR review surface, along with a deep-link into the dashboard's offending-line view. The full catalog with one-line descriptions is in the dashboard's Dashboard → PR Reviews → any review.

Evidence and config URLs

The control-config-url isn't just a pointer — it's evidence. Auditors ask "show me how this PII store satisfies your encryption-at-rest standard," you reply with the URL, they fetch the config document. The config typically declares:

Host config files alongside your other governance documents — an internal GitHub repo, a Confluence-published JSON export, or an S3 bucket with bucket policy enforcing read-only access. ArchRails just needs an HTTPS GET to work.

Worked example · encrypt PII at rest

1. Author the requirement schema at https://controls.myorg.io/encryption-at-rest.schema.json:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id":     "https://controls.myorg.io/encryption-at-rest.schema.json",
  "type":    "object",
  "required": ["algorithm", "key_length_bits", "key_manager", "rotation_days", "owner"],
  "properties": {
    "algorithm":       { "enum": ["AES-256-GCM", "AES-256-CBC", "ChaCha20-Poly1305"] },
    "key_length_bits": { "const": 256 },
    "key_manager":     { "enum": ["aws-kms", "gcp-kms", "azure-keyvault", "on-prem-hsm"] },
    "rotation_days":   { "type": "integer", "maximum": 365 },
    "owner":           { "type": "string" }
  }
}

2. Author the orders-db config at https://controls.myorg.io/orders-db/encryption.config.json:

{
  "algorithm":       "AES-256-GCM",
  "key_length_bits": 256,
  "key_manager":     "aws-kms",
  "rotation_days":   90,
  "owner":           "data-platform@myorg.io"
}

3. Attach to the node in your CALM file (see Attaching to nodes above for the syntax).

The next PR runs validation. ArchRails fetches both URLs, validates the config against the schema, and reports AR-CTRL-001 if anything fails (wrong algorithm, rotation past 365 days, missing owner). A passing PR includes a green checkmark in the review surface with the requirement name and config URL cited — auditor-ready evidence on every change.

Using the audit trail in your audit

The PR-level evidence trail is what makes controls useful for audit. Each PR that lands on a governed branch generates an immutable record of which requirements were checked and what their config evaluated to — per-control, per-merge, per-signer. Common uses:

The dashboard surfaces a per-control evidence export so you can hand auditors a JSON file rather than a dashboard tour. Mapping the evidence into your specific control-framework catalog is a customer-by-customer exercise — talk to your auditor and your engagement manager about the right way to bind the evidence to your control IDs. See Dashboard → PR Reviews → Evidence export when you're ready to ship one.

Common mistakes

I attached a control but every PR fires AR-CTRL-003.

Means one or both URLs are missing from the requirement entry. Both control-requirement-url and control-config-url are required — the schema URL alone tells ArchRails what to check, but without the config URL there's nothing to check. Add both, and if you don't yet have a config, point at an empty {} JSON file — AR-CTRL-001 will fire instead, which is the right signal ("the config doesn't satisfy the schema").

My schema fetch keeps timing out.

ArchRails caches fetched schemas per Lambda invocation, but the initial fetch needs to succeed within 30 seconds. If your control URLs are behind SSO or a corporate proxy, ArchRails can't reach them — AR-CTRL-002 fires and the requirement is skipped. Host control documents on public HTTPS or on the same internal artifact store as your other CI dependencies.

I want to suppress a built-in AR-CTRL rule for a specific node.

Reach out to support before doing this — AR-CTRL rules exist because they catch real incidents. If suppression is genuinely the right call (e.g., a node that's intentionally externally reachable and AR-CTRL-PUBLIC-EXPOSURE-001 is the wrong signal), the dashboard's per-node suppression flow records the reason and the suppressing user, so audit trail isn't lost.

Why two URL fields instead of one?

Separation of concerns. The schema is authored once by the compliance team and shared across hundreds of nodes ("our encryption-at-rest standard"). The config is per-node ("here's how this store implements it"). Putting them in one document would force every node to redeclare the schema or invent a side channel for shared schemas. Two URLs keeps both cleanly versionable.