Backend

[ AxiomCore · Documentation ]

Integrations and transports

Declare server caching, request batching, transactional batch writes, outbound HTTP, multipart uploads, SSE, WebSockets and server telemetry.

Connect a backend to caches, remote HTTP services, file storage and streaming clients with typed declarations. These declarations compile into the private server plan (.server.json). They do not change public endpoint IDs or grant authority to imported clients. Client CachePolicy, RetryPolicy and observability(...) declarations keep describing client behavior.

Every integration follows the same setup: declare a resource and the integration, build, scaffold deployment.json with axiom server deployment, then review the proposed provider and add explicit grants. The deployment must match the exact plan hash, resource interface, provider configuration schema and grants.

IntegrationDeclarationProviderGrants
Read cacheserverCachememoryCacheRESOURCE.lookup, RESOURCE.invalidate
Request batchingbatchOperation, batchPolicyA qualified batching factoryThe scalar and the batch operation
Outbound HTTPoutboundhttpClientThe operation
Multipart uploadmultipartlocalUploadThe store operation
SSE, NDJSON, WebSocketstreamPolicyNoneNone
TelemetryserverTelemetryNoneNone

Server caching

cache.acore
project(id: "cached-server", version: "1")
model Key { id: Int32 }
model Item { title: String }
guard signed(kind: GuardKind.authenticated)
resource cache(lifetime: ProviderLifetime.application) {
  operation lookup(input: Models.Key, output: Models.Item, effect: EffectKind.read)
  operation invalidate(input: Void, output: Void, effect: EffectKind.delete)
}
action read(kind: ActionKind.query, input: Models.Key, output: Models.Item) {
  native(symbol: "items.read")
  require(guard: Guards.signed)
}
action change(kind: ActionKind.command, input: Models.Key, output: Void) {
  native(symbol: "items.change")
  require(guard: Guards.signed)
}
serverCache items(action: Actions.read, resource: Resources.cache,
  ttl: 30s, isolation: CacheIsolation.principal,
  maxEntries: 256, maxBytes: 1048576, invalidateOn: [Actions.change])

Bind cache to memoryCache (no configuration values) and grant cache.lookup and cache.invalidate.

  • Keys bind the action and cache resource, the exact plan, contract, schema and codec identity, deployment grants, and validated normalized input. Credentials are never key material.
  • Isolation. principal includes the complete verified principal. tenant shares results among tenant members with identical roles, permissions and claims. public requires an unguarded query with a public result. Identity and guards run before every read, including cache hits.
  • Bounds. The cache is process-local, with TTL, byte and entry limits. Concurrent misses for a key share one fill. A fill's deadline is the action timeout capped at 30 seconds and fixed at creation. Each cache admits at most 4,096 waiters and maxEntries concurrent fills. Canceling one waiter keeps the others; the last waiter to leave cancels the fill. Only typed successful outcomes are cached. HTTP metadata is produced again for each response.
  • Invalidation. invalidateOn clears the cache across isolation namespaces after the named command succeeds; failed commands do not. A generation fence stops an older in-flight fill from repopulating it. This observes action success, not a database commit; external changes need an invalidation action or expiry.

Request batching

batchOperation derives a batch interface from a scalar resource read, and batchPolicy lets the host aggregate concurrent calls to it. Call sites stay ordinary typed reads.

batch.acore
project(id: "bounded-batch", version: "1")
model Key { id: Int32 }
model Task { title: String }
model Missing { message: String }
failure missing(payload: Models.Missing, status: 404)
resource tasks(lifetime: ProviderLifetime.application) {
  operation lookup(input: Models.Key, output: Models.Task, effect: EffectKind.read, errors: [Failures.missing])
  batchOperation lookupMany(operation: .lookup)
}
batchPolicy taskLookup(resource: Resources.tasks, operation: .lookup,
  batchOperation: .lookupMany, maxItems: 8, maxBytes: 65536, window: 100ms)
action dashboard(kind: ActionKind.query, input: Void, output: List<Models.Task>) {
  errors(Failures.missing)
  managed {
    let tasks = await parallel(limit: 3) {
      first = Resources.tasks.lookup(input: {id: 1})
      second = Resources.tasks.lookup(input: {id: 2})
      third = Resources.tasks.lookup(input: {id: 3})
    }
    return [tasks.first, tasks.second, tasks.third]
  }
}
action mixed(kind: ActionKind.query, input: Void, output: Models.Task) {
  managed {
    let tasks = await parallel(limit: 3, mode: ParallelMode.collect) {
      first = Resources.tasks.lookup(input: {id: 1})
      second = Resources.tasks.lookup(input: {id: 2})
      third = Resources.tasks.lookup(input: {id: 3})
    }
    match tasks.second {
      case .success(task) { return task }
      case Failures.missing(problem) { return {title: problem.message} }
    }
  }
}

The compiler generates Batch_tasks_lookupMany_Input (itemId: String, input: Models.Key) and Batch_tasks_lookupMany_Output (itemId: String, an optional value: Models.Task, and optional typed failure cases taken from the scalar's declarations). Each output item carries exactly one value or one failure. Missing, duplicate or unknown IDs, wrong payload types and ambiguous outcomes reject the whole malformed reply. Replies may arrive in any order; IDs keep the association. Duplicate inputs stay distinct items. lookupMany can also use an explicitly authored compatible list envelope with the same checks.

  • Scope. Calls aggregate only within one admitted root with matching principal and permission partition, attempt and deadline ceilings.
  • Activation. The provider factory must attest batch support for the exact scalar and batch pair and single-attempt read safety for both. The deployment must grant both operations and the exact interface. A declaration cannot enable a provider without this protocol. Generated envelopes need the scalar and its failures declared locally.
  • Bounds. 1 to 32 items, 1,024 to 65,536 bytes, a 1 to 100 ms window, and at most 256 active coordinators. Each root pays for its logical calls before queuing. One coordinator owns each physical dispatch, with a fixed deadline, independent cancellation and one attempt.
  • Cancellation. Canceled items leave the queue before dispatch. After dispatch, their results are suppressed while siblings continue; canceled I/O drains before capacity is released. Malformed outcomes are terminal. An eligible per-item retry policy can retry a failed item and keep successful siblings.
  • Exclusions. Commands, transaction-bound resources, invocation-lifetime providers, Workers and unqualified transports. Batching is private: it adds no public endpoint and never merges arbitrary HTTP requests. Extension guests use the host boundary only under their existing grants; generated SDK batch helpers are not available.

batchPolicy adds the requestBatching capability. Static inspection shows scalar and physical operation dependencies. At runtime, batch.dispatch observations use declared labels and omit arguments, IDs, credentials and principals. The batching behavior is qualified with a synthetic native provider (three logical calls, one physical dispatch, typed per-item failures, and rejection of unqualified factories and malformed replies); that does not certify an external production provider.

Transactional batch writes

For bounded durable ingestion, declare a typed writeBatch operation and call it inside a managed transaction. The PostgreSQL runtime specification adds an insertBatch query:

Database runtime specification excerpt
{
  "kind": "insertBatch",
  "table": "app.archive",
  "inputField": "changes",
  "maxItems": 32,
  "values": {
    "id": {"kind": "item", "field": "id"},
    "title": {"kind": "item", "field": "title"}
  },
  "fields": {"id": "id", "title": "title"}
}
  • inputField names a required typed list of records. The declaration allows 1 to 64 items, 1 to 128 checked assignments and 256 KiB of serialized items. Each item binding names a required record field with the exact storage codec. The tenant comes from the verified principal; items cannot set the tenant or generated columns.
  • Each step runs one parameterized insert for the whole batch. Business rows, outbox rows and the receipt commit together. Batch steps need a receipt and cannot use per-item savepoint recovery. A constraint, codec or output-bound failure rolls back everything. Returned rows are sorted by canonical JSON before storage, so do not infer input order from RETURNING.
  • Offline preparation validates models, columns, projections, captures, bounds and receipt compatibility and rejects unknown JSON fields. The editor validates the same wire schema and links captured inputs. Activation still verifies the signed schema, interface and grants.
  • The profile is native PostgreSQL 17.11. Other adapters do not inherit it.

Outbound HTTP

outbound.acore
project(id: "outbound-server", version: "1")
model Request { id: Int32 }
model Reply { value: Int32 }
resource service(lifetime: ProviderLifetime.application) {
  operation lookup(input: Models.Request, output: Models.Reply, effect: EffectKind.read)
}
outbound lookup(resource: Resources.service, operation: .lookup,
  method: OutboundMethod.get, path: "/value", safety: OutboundSafety.safe,
  attempts: 3, backoff: 50ms, timeout: 2s, maxResponseBytes: 65536,
  circuitThreshold: 5, resetAfter: 1s)
action fetch(kind: ActionKind.query, input: Models.Request, output: Models.Reply) {
  managed {
    let reply = await Resources.service.lookup(input: {id: Input.id})
    return reply
  }
}
endpoint fetch(method: GET, path: "/fetch/{id}") {
  request { path("id", type: Int32) }
  handle(action: Actions.fetch, input: {id: Input.path.id})
  response(type: Models.Reply)
}

outbound(resource:, operation:, method:, path:, safety:, idempotencyField:, attempts:, backoff:, timeout:, maxResponseBytes:, circuitThreshold:, resetAfter:). Methods are get, head, post, put, patch and delete.

Bind service to httpClient. The scaffold proposes four environment references: origin, credential, allowLoopback and idempotencySupported (for this example, AXIOM_HTTP_SERVICE_ORIGIN and so on). The resolver reads them once at startup. credential is the whole Authorization header value, or an empty string; rotating it through the environment needs a restart.

  • The origin is HTTPS with no path, query, fragment or user information. HTTP is accepted only for explicit loopback development with allowLoopback=true. Redirects and proxy discovery are disabled, and caller data never chooses the destination.
  • GET and HEAD input fields become percent-encoded query values; other methods send a typed JSON body. Responses must be unambiguous JSON of the exact output type; HEAD requires Void.
safetyAllowed methods and retries
safeGET, HEAD
idempotentPUT, DELETE
idempotencyKeyNeeds a required String idempotencyField: .key, a stable key of 8 to 128 ASCII characters, and a deployment that confirms downstream support
unsafeOne attempt

Retries cover transport failures and transient 429 and 5xx responses; validation failures are terminal. Eligibility follows HTTP method semantics. An ambiguous unsafe dispatch returns outbound_indeterminate, which the delivery worker never replays automatically. Declaring a key does not make the remote service idempotent.

The operation deadline includes retries and backoff, and cancellation closes in-progress I/O. Each root invocation shares a 16-attempt outbound budget across nested actions; one operation allows at most four attempts. Each HTTP resource allows 16 concurrent logical calls. Circuits track logical destination failures, open for resetAfter, and allow one recovery probe. Framework logs omit credentials, URLs and bodies. A read classification does not attest the remote side's replay semantics.

The environment resolver is the installed provider workflow. The matching source preview also offers purpose-bound runtime credentials and explicit local and Cloud bindings, with a separate target and distribution gate; see Secrets at runtime.

Multipart uploads

upload.acore
project(id: "upload-server", version: "1")
model Upload { id: String size: Int64 sha256: String mediaType: String }
model Input { file: Models.Upload label: String }
guard signed(kind: GuardKind.authenticated)
resource files(lifetime: ProviderLifetime.application) {
  operation store(input: Models.Upload, output: Models.Upload, effect: EffectKind.fileWrite)
}
action save(kind: ActionKind.command, input: Models.Input, output: Models.Upload) {
  native(symbol: "files.save")
  effect(resource: Resources.files, operation: .store)
  require(guard: Guards.signed)
}
endpoint upload(method: POST, path: "/upload") {
  request { body("file", type: Models.Upload) body("label", type: String) }
  handle(action: Actions.save, input: {file: Input.body.file, label: Input.body.label})
  response(type: Models.Upload)
}
multipart upload(endpoint: Routes.upload, resource: Resources.files,
  operation: .store, fileField: "file", maxBytes: 1048576,
  mediaTypes: ["text/plain", "image/png"])

The native callback dispatches the files.store effect and returns its descriptor; a managed action can await the same operation. An explicit effect(...) keeps source provenance and permits only that typed dispatch; native internals still have unknown enforcement evidence.

Bind files to localUpload with a root environment reference and grant files.store. The Unix storage root must be absolute, canonical, owned by the server, mode 0700, and free of symlinks. One host owns its advisory lock. Restart removes orphaned generated .part files; other names are kept. Directories over 65,536 entries are rejected. You own retention and disk quotas for confirmed .data files.

Upload limitValue
File size16 MiB, and at most maxBytes
Text field8 KiB
Body fields16
Active uploads per storage resource8
  • The transport accepts one file part and the declared fields, and checks field uniqueness and types. File bytes stream into a generated mode 0600 name while limits are checked.
  • Text must be nonempty UTF-8 without NUL or executable prefixes. PNG, JPEG and PDF get bounded signature checks. This is not malware scanning.
  • Caller filenames never become paths. Staged files belong to an unforgeable invocation token.
  • Identity and simple guards run before file data is read. Typed input and ownership guards run before storage. Files are confirmed after typed success and cleanup. Failures, invalid content, disconnects, deadlines and drain delete generated files.
  • Upload actions cannot use idempotent replay. Legacy strict security.uploads declarations remain unsupported and fail closed.

SSE, HTTP streams and WebSockets

The host serves the public SseStreamDef, JSON message HttpStreamDef and bidirectional WebSocketDef contracts described in Streams and WebSockets. Each stream endpoint needs a private streamPolicy. File, binary and other framing or direction profiles fail at startup.

stream.acore
project(id: "stream-server", version: "1")
model Frame { id: String value: Int32 }
model Cursor { last: Optional<String> }
model Message { value: Int32 }
model SocketInput { message: Models.Message }
guard signed(kind: GuardKind.authenticated)
serverPolicy lifetime(timeout: 1m)
action events(kind: ActionKind.stream, input: Models.Cursor, output: Models.Frame) {
  native(symbol: "events.produce")
  require(guard: Guards.signed)
  server(policy: ServerPolicies.lifetime)
}
action reply(kind: ActionKind.stream, input: Models.SocketInput, output: Models.Message) {
  native(symbol: "socket.reply")
  require(guard: Guards.signed)
  server(policy: ServerPolicies.lifetime)
}
endpoint events(method: GET, path: "/events") {
  request { header("last-event-id", type: String, optional: true) }
  handle(action: Actions.events, input: {last: Input.header["last-event-id"]})
  response(type: Models.Frame)
  streaming(SseStreamDef(responseType: Models.Frame))
}
endpoint socket(method: WS, path: "/socket") {
  request { body("message", type: Models.Message) }
  handle(action: Actions.reply, input: {message: Input.body.message})
  response(type: Models.Message)
  streaming(WebSocketDef(direction: "bidirectional",
    client: WsMessageDef(model: Models.Message),
    server: WsMessageDef(model: Models.Message), reconnect: "auto"))
}
streamPolicy events(endpoint: Routes.events, buffer: 2, maxFrameBytes: 16384,
  maxFrames: 1000, heartbeat: 10s, stallTimeout: 5s, maxConnections: 16, idField: .id)
streamPolicy socket(endpoint: Routes.socket, maxFrameBytes: 16384,
  maxFrames: 1000, heartbeat: 10s, stallTimeout: 5s, maxConnections: 16,
  origins: ["https://app.example.com"])

streamPolicy(endpoint:, buffer:, maxFrameBytes:, maxFrames:, heartbeat:, stallTimeout:, maxConnections:, origins:, idField:).

TransportWire format
SSEtext/event-stream, comment heartbeats, optional bounded event IDs, and Last-Event-ID as input for application-defined resume (SSE specification)
HTTP streamapplication/x-ndjson, one JSON frame per line, blank-line heartbeats that clients skip
WebSocketJSON messages, serial processing, exact Origin checks
  • A native producer validates frames through a sealed sink with bounded queue waits. Managed, sandbox and ordinary native stream actions emit one typed successful frame. The host does not invent durable event history.
  • SSE and HTTP streams require read-only declared effects.
  • WebSockets authorize before the upgrade and before each message, action and output. Handshake guards cannot depend on message data. Ping and pong heartbeats refresh credentials. An ambiguous unsafe effect closes the socket with code 1011 and reason outbound_indeterminate; clients must not replay that message automatically.
  • Slow readers, revoked credentials, disconnects, lifetime expiry and drain close the connection and cancel cooperative callbacks.
  • The whole host allows 64 stream and WebSocket connections. Queues hold at most eight frames of at most 64 KiB each.
  • Native WebSocket clients can send header credentials. Browsers send session CSRF proof through the subprotocols axiom.json and axiom.csrf.<base64url-without-padding>; only axiom.json is selected in the response. Reconnection intent never bypasses fresh authorization.
  • Generated Python and Go interfaces include typed ServerFrame_*, ClientMessage_* and frame iterator and sink aliases.

Server telemetry

Acore excerpt
serverTelemetry(maxSeries: 256, maxEvents: 64)

The trusted host keeps bounded snapshots of observations and cache, upload, stream and batch usage. There is no public metrics endpoint. Labels use plan names; series beyond maxSeries aggregate into overflow. Counters saturate, and trace events use a bounded ring. Endpoints, policies, actions, resources, outbound calls and job or consumer attempts share W3C trace IDs with child spans. Framework logs omit credentials, claims, payloads, URLs and filenames; custom native logging is the host author's responsibility.

Verify

Check contract output, generated client types and real provider behavior on the host you deploy. Exercise uploads, streams, backpressure, cancellation, cleanup and the configured bounds. Run authentication, managed action and durable Redis checks when you use those features. Native callbacks must honor cancellation; a declaration cannot certify their allocations or I/O. Deployment certification, database outbox guarantees, distributed cache coherence and other transport or storage providers have separate support boundaries.

Next steps