Acore language

[ AxiomCore · Documentation ]

Domain and security models

Add optional entities, relationships, invariants, projections and security policy to a backend contract, compiled into signed manifests.

Domain Model v1 and Security Model v1 are optional layers on top of a backend contract. The domain layer names the application concepts behind your models; the security layer declares who may call what, and with which data and side effects. Both compile into canonical manifests inside the .axiom artifact. Examples were checked with Axiom CLI 0.149.1 (axiom check, axiom domain validate, axiom security check).

Both layers are part of the Acore language toolchain (Alpha). Bootstrapping a domain block from OpenAPI or SQL is Experimental.

How domain and security declarations reach a signed artifact and review
How domain and security declarations reach a signed artifact and review
Diagram source
flowchart LR
  A["Models from source or extractor"] --> B["entity, relationship, invariant, projection"]
  A --> C["security(...) declaration"]
  B --> D["axiom-domain-manifest/v1"]
  C --> E["axiom-security-manifest/v1"]
  D --> F[".axiom artifact"]
  E --> F
  F --> G["Release signing, pull request review, impact view"]

Domain Model v1

The domain layer describes application concepts; it is not another copy of your schema. An extractor for FastAPI or Go discovers models, field validation and association candidates, and you promote the concepts that matter. The compiler owns all semantic IDs, picks conventional id or uuid keys, resolves promoted entities, folds extractor relationship candidates into the manifest, and signs the canonical payload as part of the normal release artifact.

Models holds the backend's models. Entities is a separate vocabulary for semantic boundaries:

  • Append also entity to a local model and refer to it with the model's case, Entities.Task, like Models.Task.
  • Or promote a model with entity name(model: Models.Name) and refer to Entities.name.
backend/axiom.acore
// Models and routes come from the existing service.
amends "axiom-fastapi:./main.py:app"

entity customer(model: Models.Customer, doc: "A purchaser with a durable account.")
entity order(model: Models.Order)

// Customer.orders and Order.customer are discovered from the source model.
invariant orderTotal(
  scope: Entities.order,
  expression: "total_cents >= 0",
  fields: ["total_cents"]
)
projection publicCustomer(entity: Entities.customer, fields: [.id, .email])

override endpoint Routes.get_customer {
  response(type: Entities.customer, projection: Projections.publicCustomer)
}
DeclarationSignature
entityentity name(model: Models.Name, doc: String?, key: List<String> = [])
relationshiprelationship name(from: Entities.a, to: Entities.b, via: String, inverse: String, cardinality: Cardinality, ownership: Ownership, required: Boolean = false, doc: String?)
invariantinvariant name(scope: Entities.a, expression: String, fields: List<String>, rules: Json?, severity: String = "error", doc: String?)
projectionprojection name(entity: Entities.a, fields: [.field], audience: Audience.name?, doc: String?)

Constructor calls use named arguments, such as DomainInvariant(scope: ..., fields: [...]). Literal objects use colon-separated fields and commas.

Identity

The model argument is a resolved model reference. Never repeat a model name as a string or assign a domain id. The key is inferred from id, uuid or a case-insensitive equivalent; only an unconventional durable key needs key:

Acore excerpt
// An unconventional entity key is explicit; IDs remain compiler-owned.
entity invoice(model: Models.Invoice, key: ["invoice_number"])

Entity arguments can add documentation or entity-level metadata without a second model declaration.

Relationships

The Python and Go extractors recognize direct model fields and model collections. A bidirectional association becomes one relationship candidate with via and inverse. A candidate is included only after both models are promoted, so not every DTO becomes a domain concept. When an association cannot be inferred, declare it with Entities.* and the enum helpers instead of raw wire values:

Acore excerpt
relationship orderCustomer(
  from: Entities.order,
  to: Entities.customer,
  via: "customer_id",
  cardinality: Cardinality.manyToOne,
  ownership: Ownership.reference
)

Cardinality: oneToOne, oneToMany, manyToOne, manyToMany. Ownership: owned, reference, shared. The compiler checks via on the source entity and inverse on the target, defaults an omitted ownership to reference, and derives cardinality from a collection field when it can. These declarations never run a database migration or change runtime ownership.

Invariants and Rod rules

An invariant is evidence for review, generation and impact analysis, not code that runs. It can be a bounded readable expression, a structured Rod validation rule, or both. Its scope must be an Entities.<name> reference.

Acore excerpt
invariant validShippingAddress(
  scope: Entities.order,
  fields: ["shipping_address"],
  rules: {
    type: "object",
    properties: { postal_code: { type: "string", min: 3, max: 16 } },
    required: ["postal_code"]
  }
)

Field validation extracted from Pydantic and other Rod-compatible schemas becomes inferred *.rod invariants when its model is promoted. Rule objects have depth, property and collection limits and allow only the supported Rod kinds, so validation cannot become an arbitrary code execution channel.

Compile, validate and generate

Terminal
axiom domain validate axiom.acore
axiom build axiom.acore --variant default --out backend.axiom

For a contract with three entities, axiom domain validate axiom.acore --json prints:

Example output of axiom domain validate --json
{
  "endpoint_bindings": 1,
  "entities": 3,
  "format": "axiom-domain-manifest/v1",
  "hash": "fa92f9ab770294948a968f1abb159f217b3e9e8551f8a3505c21a0e7654c28fa",
  "invariants": 2,
  "projections": 1,
  "relationships": 1
}

Without --json, it prints the entity, relationship and projection counts and the manifest hash, or a note that no Domain Model v1 block is declared. axiom build validates the resolved domain and embeds the manifest in the .axiom artifact. The manifest is canonicalized before its hash is calculated, and Cloud release signing covers those exact bytes.

Generated ATMX clients keep the transport models and add typed projection aliases. A response bound to publicCustomer uses its allowlist, while the runtime keeps validating the signed transport contract.

Review and impact

The dashboard stores the compiled semantic manifest beside the signed release. Axiom pull requests classify entity, relationship, invariant, projection and binding changes. The impact view follows the dependency graph to affected endpoints and routes and attaches observed consumer telemetry for the source release. The member-only semantic endpoint is:

Axiom Cloud API route
GET /api/v1/projects/{project_slug}/contracts/{version}/semantic-view

It requires project access even for public projects and excludes raw contract bytes, source code, credentials and runtime payloads.

Bootstrap from an existing schema

Experimental OpenAPI JSON and exported SQL DDL can produce a reviewable promotion suggestion:

Terminal
axiom domain bootstrap openapi.json --source openapi --output axiom.domain.acore
axiom domain bootstrap schema.sql --source sql --output axiom.domain.acore

The command reads a file of at most 8 MiB, never inspects a live database, and refuses to overwrite an existing --output file (default axiom.domain.acore). Its output uses model references, Entities.* and inferred defaults, and assumes matching extracted model symbols exist in the contract. Review projections and any unconventional key before release.

The public domain-inference-fastapi example is a compact source-to-manifest fixture with Pydantic validation and a bidirectional Customer and Order association; its Acore file promotes only the two entities. Its README lists the local commands.

Security Model v1

The security layer covers authorization, authentication, data boundaries, side effects and application-wide requirements. Go and Python extractors may report evidence without adding an SDK to your application, but only a contract can declare a policy. The compiler canonicalizes declarations, AXSEC findings, coverage, the policy graph and extractor facts into axiom-security-manifest/v1, which the .axiom artifact signs with the rest of the contract. The manifest never contains a token, cookie value, API key or request payload.

Modes

ModeBehavior
audit (default)Reports every finding and still builds and releases, so a team can form a baseline.
strictTreats findings as blocking. A build or release with a blocking AXSEC finding fails: security strict mode rejected build: 1 blocking AXSEC finding(s).

Structural errors, such as an unknown policy reference or an unsupported invariant kind, always stop compilation because the declaration would be ambiguous.

Acore excerpt
// Add to a contract declaring Entities.document and UpdateDocument.
security(
  mode: "audit",
  principals: { workspaceMember: SecurityPrincipal(roles: ["member", "owner"]) },
  auth: {
    browserSession: AuthenticationPolicy(
      transport: "cookie", secure: true, httpOnly: true,
      sameSite: "lax", sessionRotation: true
    )
  },
  permissions: {
    editDocument: Permission(
      requiresRateLimit: true, requiresIdempotency: true, requiresCsrf: true
    )
  },
  resources: { document: ProtectedResource(entity: Entities.document) },
  policies: {
    documentEditor: SecurityPolicy(
      principals: ["workspaceMember"], auth: "browserSession", resource: "document",
      permissions: ["editDocument"], relationship: "workspaceDocument",
      resourceBinding: "principal.workspaceId == resource.workspaceId"
    )
  },
  endpoints: {
    UpdateDocument: EndpointGuard(
      policy: "documentEditor", resource: "document", permission: "editDocument",
      writableFields: ["title", "body"], idempotent: true
    )
  },
  rateLimits: { memberWrite: RateLimit(maxRequests: 60, windowSeconds: 60, key: "principal") },
  csrf: CsrfPolicy(strategy: "double_submit", sameSite: "lax", secure: true, httpOnly: true)
)
Terminal
axiom security check axiom.acore
Example output
Security Mode audit · 0 error(s), 1 warning(s) · 1 of 1 endpoints covered (1 direct, 0 default-policy)
AXSEC-061 WARNING [UpdateDocument] `UpdateDocument` uses permission `editDocument` which requires rate limiting.
  → Bind a declared rate limit to this endpoint.

Adding rateLimit: "memberWrite" to the EndpointGuard clears the warning. axiom security check accepts --json, and --fail-on-warning returns a non-zero result for a review-only CI job.

Principal, permission, policy, authentication-profile, resource and rate-limit names belong to your application. AxiomCore does not keep a hard-coded list of business action types.

Security declarations

ArgumentValue
mode"audit" or "strict"
principalsSecurityPrincipal(roles: [...], doc)
authAuthenticationPolicy(transport, issuer, audience, secure = false, httpOnly = false, sameSite = "lax", sessionRotation = false, validateExpiry = true, doc)
permissionsPermission(requiresRateLimit = false, requiresIdempotency = false, requiresCsrf = false, doc)
resourcesProtectedResource(entity: Entities.name, doc)
policiesSecurityPolicy(principals, roles, auth, resource, permissions, relationship, resourceBinding, doc)
defaultPolicyName of a project-wide fallback policy
endpointsEndpointGuard(policy, resource, permission, auth, rateLimit, effects, writableFields, idempotent = false, public = false, csrfExempt = false)
rateLimitsRateLimit(maxRequests, windowSeconds, key)
csrfCsrfPolicy(strategy, sameSite = "lax", secure = true, httpOnly = true)
corsCorsPolicy(origins, credentials = false, methods, headers)
classificationsSensitiveData(target, classification, audiences, redact = false, doc)
services, effects, uploadsSee Declared side effects
invariantsSecurityRequirement(kind, target, severity = "error", doc)

Authorization and authentication

  • ProtectedResource.entity takes an Entities.* reference. The compiler resolves it to the promoted model and its compiler-managed entity ID in the signed manifest.
  • A route with a resource parameter should bind a policy and state resourceBinding. AXSEC-014 makes missing IDOR evidence visible.
  • defaultPolicy is a reviewable project-wide fallback and must name a declared policy. Every public endpoint must set public: true; neither a missing security block nor an undeclared endpoint is silently treated as protected.

Authentication profiles describe transports, not providers:

transportRequirement
bearerRequires issuer and audience
cookieRequires sameSite of lax, strict or none. The analyzer reports missing secure or httpOnly flags and missing CSRF coverage for browser mutations.
mTLS and adapter-specific transportsAllowed where a trusted server adapter verifies them. Server secrets stay outside the contract.

Attach a profile to a SecurityPolicy as a reusable default, or to an EndpointGuard as a route-specific override.

Data and output boundaries

Use classifications to label extracted model fields without repeating schemas. audiences is optional, but required when a projection intentionally exposes classified data.

Acore excerpt
// Include classifications in the contract's security(...) declaration.
security(classifications: {
  accountEmail: SensitiveData(
    target: "Account.email", classification: "PII",
    audiences: ["workspaceMember"], redact: true
  )
})

For an endpoint that returns a model with classified data, bind a domain response projection. AXSEC-081 reports an unprojected return, and AXSEC-082 reports a classified projection field with no audience. This is an allowlist boundary, not runtime response inspection.

Declared side effects

Effects make sensitive sinks reviewable without imperative code:

Acore excerpt
// Include these arguments in the contract's security(...) declaration.
security(
  services: { billing: NetworkService(origin: "https://api.example.com") },
  effects: { createCharge: SecurityEffect(kind: "network", service: "billing") },
  uploads: {
    avatar: UploadPolicy(
      maxBytes: 5242880, mediaTypes: ["image/jpeg", "image/png"],
      inspectContent: true, generatedFilename: true,
      privateStorage: true, executable: false
    )
  }
)
  • Effect kinds: network, html_output, upload, filesystem_read, filesystem_write, process, log, analytics, cache, response.
  • Network, upload, filesystem and process effects bind to declared policies instead of user-controlled destinations.
  • NetworkService defaults: allowPrivateNetworks = false, allowRedirects = false. UploadPolicy defaults: inspectContent, generatedFilename and privateStorage are true, executable is false; maxBytes is required.

Whole-application requirements

invariants use bounded, deterministic kinds, not an expression language:

KindRequires
require_authorizationAuthorization on the target
require_rate_limitA rate limit
require_idempotencyIdempotency
require_csrfCSRF protection
no_secret_exposureNo secret exposure
declared_network_onlyOnly declared network services
no_public_cacheNo public caching
declared_projection_onlyOnly declared projections
Acore excerpt
invariants: { csrfOnWrites: SecurityRequirement(kind: "require_csrf", target: "UpdateDocument") }

severity is "error" or "warning". The application-owned target is signed and diffed with the same AXSEC rule families. An unsupported kind fails with AXB004: security invariant ... has unsupported kind.

See Security rules for the complete AXSEC catalog, extractor evidence and the enforcement boundary.