[ 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
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
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.
| Declaration | Arguments |
|---|---|
permission name | claim |
guard name | kind: GuardKind.authenticated, or permission, or role, or an ownership lookup: resource, operation, input, claim, field |
serverPolicy name | timeout, attempts, maxInFlight, overrides, guards |
serverDefaults | policy |
require (action or endpoint) | guard |
rateLimit (action or endpoint) | requests, window, key (principal, tenant, global), distributed |
idempotency (action or endpoint) | header, retention, durable |
cors | origins, 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 anoverridesentry such asServerPolicyField.timeoutMs,maxAttemptsormaxInFlight; 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.
.lookupand.ownerresolve against the resource and the operation's output model.Principal.subjectandPrincipal.tenantare the typed claim choices; misspellings are compile errors. The editor completes, navigates and renames these references across imports. Declarations can be imported withamends; 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.
axiom check service.acore
axiom build service.acore --out backend.axiom
axiom server inspect backend.axiom backend.server.jsonserver 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
axiom server deployment backend.axiom backend.server.json --output deployment.jsonThe 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.
{
"kind": "jwt",
"issuer": "https://issuer.example",
"audience": "task-server",
"algorithm": "RS256",
"keyEnv": "AXIOM_JWT_PUBLIC_KEY"
}| Provider | Configuration | Rules |
|---|---|---|
jwt | issuer, audience, algorithm, keyEnv | HS256, 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. |
apiKey | header, registryFileEnv | Dedicated header. The registry file maps lowercase SHA-256 token digests to principal and expiresUnixSeconds. |
session | cookie, registryFileEnv, csrfHeader, origins | Verifies 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, audience | Verifies 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. |
{"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:
{
"action": "getTask",
"origin": "job",
"identity": "service",
"input": {"path.id": 1},
"headers": {"authorization": "Bearer <provider-issued-token>"}
}originisdirect,joborconsumerand must match the entry point the trusted host chose. A worker cannot downgrade to direct-call rules.identityiscaller,serviceordelegated. Inputs use private-plan names; an extracted path parameter ispath.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 matchingclaims["axiom.action"]and a nonemptyclaims["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 returns409. - 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
Run a backend locally
Run an Acore backend on your machine with real actions, reload, mocks or full server simulation, and inspect what the runtime enforces.
Events, jobs and webhooks
Declare typed events, jobs, schedules and signed webhooks, and run them with durable Redis delivery and guarded attempts.