Backend

[ AxiomCore · Documentation ]

Identity and policies

Verify callers, enforce mandatory guards, and apply CORS, rate limits and idempotency at the Acore server boundary.

Protect a backend by declaring who may call each action and how requests are admitted. Server policies compile into the private execution plan (backend.server.json). The client cache(...) and retry(...) declarations keep their separate client meanings.

Admission order

How the server admits a protected request
How the server admits a protected request
Diagram source
flowchart LR
  A[Request] --> B[CORS preflight]
  B --> C[Verify identity]
  C --> D[Action and endpoint guards]
  D --> E[Rate limit and idempotency]
  E --> F[Action]
  F --> G[Effect gate and grants]

Valid CORS preflights are answered before authentication. Identity is verified next, then every guard runs. Rate-limit counters are consumed only after authentication and guards pass, and an idempotency claim lets one invocation through per key. Each provider call the action makes then passes the effect gate.

Declare policies

service.acore
project(id: "task-service", version: "1.0.0")
variant default(include: ["*"])
model Task { title: String completed: Bool }
model Owner { owner: String tenant: String }
model Failure { reason: String }

permission readTask(claim: "task.read")
guard signedIn(kind: GuardKind.authenticated)
guard reader(permission: Permissions.readTask)
resource owners(lifetime: ProviderLifetime.invocation) {
  operation lookup(input: Int32, output: Models.Owner, effect: EffectKind.read)
}
guard owner(
  resource: Resources.owners,
  operation: .lookup,
  input: Input.path.id,
  claim: Principal.subject,
  field: .owner
)
failure unavailable(payload: Models.Failure, status: 503)
serverPolicy global(timeout: 10s, guards: [Guards.signedIn])
serverPolicy read(timeout: 2s, overrides: [ServerPolicyField.timeoutMs])
serverDefaults(policy: ServerPolicies.global)

service api(prefix: "/api") {
  group v1(prefix: "/v1", policy: ServerPolicies.read) {
    endpoint getTask(method: GET, path: "/tasks/{id}") {
      request { path("id", type: Int32) }
      require(guard: Guards.reader)
      require(guard: Guards.owner)
      rateLimit(requests: 20, window: 1m, key: RateLimitKey.principal, distributed: true)
      response(type: Models.Task)
      errors(Failures.unavailable)
    }
  }
}

The endpoint path is /api/v1/tasks/{id}. service and group compose prefixes; they do not start a host.

DeclarationArguments
permission nameclaim
guard namekind: GuardKind.authenticated, or permission, or role, or an ownership lookup: resource, operation, input, claim, field
serverPolicy nametimeout, attempts, maxInFlight, overrides, guards
serverDefaultspolicy
require (action or endpoint)guard
rateLimit (action or endpoint)requests, window, key (principal, tenant, global), distributed
idempotency (action or endpoint)header, retention, durable
corsorigins, methods, headers, credentials, maxAge

Without a policy, an action runs with a 30-second timeout, one attempt and at most 128 in-flight invocations.

  • Precedence. Defaults, then service, outer groups, inner groups, then the endpoint's server(policy: ...). Changing an inherited value requires an overrides entry such as ServerPolicyField.timeoutMs, maxAttempts or maxInFlight; unused overrides are errors.
  • Guards add up. Endpoint guards add to the action's mandatory guards and can never remove them. An empty endpoint guard list never makes a guarded action public. Execution limits stay bounded by the action's own budget.
  • Typed references. .lookup and .owner resolve against the resource and the operation's output model. Principal.subject and Principal.tenant are the typed claim choices; misspellings are compile errors. The editor completes, navigates and renames these references across imports. Declarations can be imported with amends; local declarations keep imported server defaults.
  • Mutations. response(type: Models.Task, status: 201) declares the success status, which native handlers must return. idempotency(retention: 1h, durable: true) requests persistent replay protection.
Terminal
axiom check service.acore
axiom build service.acore --out backend.axiom
axiom server inspect backend.axiom backend.server.json

server inspect shows effective limits and the policy names they came from. Guard and provider settings never enter the public endpoint JSON or codec layout, so adding a guard need not change a client artifact. Review both artifacts when behavior changes. Extracted contracts without authored server settings can keep a JSON plan.

Configure the deployment

Terminal
axiom server deployment backend.axiom backend.server.json --output deployment.json

The scaffold is linked to the exact plan hash; regenerate it after any plan change. For the example above it proposes a jsonSnapshot provider for owners (path in AXIOM_RESOURCE_OWNERS_FILE), JWT authentication with RS256 and key AXIOM_JWT_PUBLIC_KEY, and Redis coordination in AXIOM_REDIS_URL. It contains environment variable names, never values. Grants start empty: binding a resource does not authorize its use. Review the provider choices, issuer, audience and grants. Unknown providers and unsupported guarantees prevent startup.

Framework adapters take the same deployment. FastAPI: NativeBoundary(["axiom", "server", "boundary"], "backend.axiom", "backend.server.json", deployment="deployment.json"). Go: axiom.StartCLIConfigured("axiom", "backend.axiom", "backend.server.json", "deployment.json"). See Deployment targets.

Verify identity

The deployment's authentication selects one provider.

deployment.json (authentication)
{
  "kind": "jwt",
  "issuer": "https://issuer.example",
  "audience": "task-server",
  "algorithm": "RS256",
  "keyEnv": "AXIOM_JWT_PUBLIC_KEY"
}
ProviderConfigurationRules
jwtissuer, audience, algorithm, keyEnvHS256, RS256, ES256 or EdDSA, fixed by the deployment. Signature, issuer, audience, subject and expiry are required; future nbf or iat and ambiguous claim documents fail. HS256 keys need at least 32 bytes. Keys load at startup; a key change needs a new process.
apiKeyheader, registryFileEnvDedicated header. The registry file maps lowercase SHA-256 token digests to principal and expiresUnixSeconds.
sessioncookie, registryFileEnv, csrfHeader, originsVerifies externally issued opaque cookies. Records also hold a csrfSha256 digest. Unsafe methods need the correct CSRF proof and an allowed exact Origin. Duplicate session cookies fail.
axiomAuth (Coming soon)issuer, audienceVerifies managed authentication access tokens (EdDSA) against the environment issuer's published keys and epoch. No key reference. Native and Linux container targets only. See Verify access tokens.
deployment.json (API key)
{"kind":"apiKey","header":"x-api-key","registryFileEnv":"AXIOM_KEYS_FILE"}

The registry variable holds an absolute path to a JSON file. Raw keys never appear in it. The file must be a regular file of at most 1 MiB and 4,096 records, mode 0600 on Unix (no group or other access). Tokens are 16 to 4,096 characters. Each request rereads the file, so an atomic replacement rotates or revokes keys without a restart. Missing or malformed provider data denies access with 503. The session provider is a bounded file provider, not a distributed session service; your application issues, rotates and expires cookies and sets Secure, HttpOnly and SameSite.

A verified principal has a subject, an optional tenant, roles, permissions and bounded string claims. JWT scope values add permissions. Request bodies and identity-looking headers cannot supply trusted claims. A configured provider must also satisfy the contract's declared authentication method.

Guards and ownership

Actions can require authenticated, permission, role or ownership guards. Extracted required scopes follow the action.

An ownership guard names a typed resource operation, an action input field, a trusted principal claim and a resource output field. The output field must be a required String. Principal.subject checks ownership; Principal.tenant checks tenant membership. Missing claims, missing resources and mismatches deny access. The operation must be read-only and separately granted.

The built-in jsonSnapshot provider takes a path environment reference to a bounded file shaped as operation, then scalar key, then typed output. It checks every record at startup and each result when used. Use it for controlled fixtures and static authorization data; it is not a database.

Security manifests

security(mode: SecurityMode.audit, ...) manifests are advisory. Inspection labels them auditOnly and the boundary emits security.audit; they authorize nothing, while typed guards still apply. Strict manifests support global permission policies, a single required role, a named principal category with exactly one role, named bearer JWT profiles with exact issuer and audience, writable-field allowlists, CORS and distributed rate limits. Those guards follow the action and cannot be removed from a hand-edited plan. Known model fields outside a writable allowlist are rejected, which prevents mass assignment even for optional fields. A strict idempotency requirement also needs an authored durable policy with an explicit retention.

A resourceBinding can use the bounded form principal.claim == resource.field. The resource must resolve to a domain entity with a required String field, and the action must also require a typed ownership guard comparing the same claim and field through a read-only resolver that returns that entity's model. Binding text alone creates no resolver or grant. principal.sub and principal.subject are the same claim.

Legacy relationship joins, compound principal categories, multiple roles without explicit any or all semantics, native-effect restrictions, session issuance or rotation, and legacy classification, cache, observability and invariant requirements need execution adapters that do not exist yet. Strict contracts that require them fail clearly. See Domain and security models.

Direct, job and consumer invocations

Direct calls (axiom server invoke), job attempts and event consumer attempts use the same input validation, verified identity, guards, writable-field checks, rate counters and idempotency as HTTP. Each request names its origin:

request.json
{
  "action": "getTask",
  "origin": "job",
  "identity": "service",
  "input": {"path.id": 1},
  "headers": {"authorization": "Bearer <provider-issued-token>"}
}
  • origin is direct, job or consumer and must match the entry point the trusted host chose. A worker cannot downgrade to direct-call rules.
  • identity is caller, service or delegated. Inputs use private-plan names; an extracted path parameter is path.id. Duplicate keys, unknown fields and caller-supplied principal objects are rejected.
  • Jobs and consumers need explicit credentials on every attempt. A service credential attests claims["axiom.identity"] = "service". A delegated credential attests "delegated", a matching claims["axiom.action"] and a nonempty claims["axiom.delegate"]. These claims come from a verified JWT or the deployment's API-key registry; choosing a label grants nothing.
  • Browser sessions cannot authorize background work. Revocation, expiry, tenant membership and permissions are checked again on every attempt and replay.

Native code receives an invocation-scoped context only after admission and cannot keep it after the callback returns. The context checks cancellation and deadlines before provider calls and before accepting output. Native code must cooperate with cancellation. Concurrency slots stay occupied until callbacks exit, including on failure and panic. These native entry points need one existing endpoint binding; required interceptors and HTTP response metadata need a capable host.

Effects and grants

Every provider call checks the active invocation, the action's declared effect, query safety, the typed resource signature and a separate deployment grant before any I/O. An invocation may make at most 256 provider attempts, counting failures. Cancellation, expired deadlines, undeclared operations and unavailable providers deny the call.

The boundary reports guard enforcement and observed provider operations separately. Arbitrary native handler effects remain unknown. Effect traces hold operation labels, result codes and trace context, never claims, arguments or outputs. Output validation and cancellation cannot undo an effect that already happened.

CORS and security headers

cors(origins:, methods:, headers:, credentials:, maxAge:) sets exact origins, methods and headers. Credentialed CORS forbids wildcard origins. Valid preflights must target a declared route. Error responses to an allowed origin keep their CORS headers. Policy-controlled responses carry nosniff, a no-referrer policy and X-Frame-Options: DENY. HTTP responses default to Cache-Control: no-store unless supported response metadata declares a reviewed cache policy; this is separate from server caching and client cache policies.

Rate limits

rateLimit keys are principal (which includes the tenant), tenant or global. Windows are fixed and start with the first request in a new window. The memory provider is bounded to one process; distributed: true requires Redis. Limits are shared across HTTP, direct, job and consumer calls.

Idempotency

idempotency(header:, retention:, durable:) applies to guarded commands. Keys are scoped by project, HTTP binding (or @action:<name> for direct, job and consumer calls), tenant and principal. Fingerprints include the plan hash and the canonical typed input.

  • Reusing a key with different input returns 409. A duplicate that is still in progress returns 409.
  • One invocation enters the handler. After output validation, its response is recorded for replay, across workers. Replays recheck authentication and guards and skip the handler. Records are limited to 64 KiB and keep validated cookies.
  • If execution fails, expires or disconnects after it may have caused an effect, the key becomes indeterminate. A worker must not repeat that effect automatically. Reconcile the external system before using a new key.
  • Protection ends when the retention ends. This is not exactly-once execution or a database transaction.
  • Upload actions cannot use idempotent replay.

For Redis, the deployment uses {"kind":"redis","urlEnv":"AXIOM_REDIS_URL","namespace":"backend"} with a redis:// or rediss:// URL. Durable idempotency requires appendonly yes, appendfsync always and maxmemory-policy noeviction, plus permission to read those settings at startup. The supported guarantee is recovery after one Redis server and its workers restart with intact storage. Failover, lost storage and external atomicity are not covered. Provider errors deny admission; there is no memory fallback.

Configuration and owned operations

Typed configuration checks required and default fields and pure validation at trusted startup. Bindings keep references and exact configuration and interface digests. A resolved configuration is not a grant. Opaque signRequest secret handles have no operation that reveals their bytes. See Secrets at runtime.

operationPolicy sets one resource boundary's total and attempt deadlines, typed retry cases, replay evidence, bounded admission and circuit breaking. Provider activation verifies safety and retry ownership separately: a resource policy needs matching trusted factory evidence, and an inner automatic retry owner cannot silently multiply a retrying operation policy. See Managed logic and concurrency.

Next steps