[ 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.
| Integration | Declaration | Provider | Grants |
|---|---|---|---|
| Read cache | serverCache | memoryCache | RESOURCE.lookup, RESOURCE.invalidate |
| Request batching | batchOperation, batchPolicy | A qualified batching factory | The scalar and the batch operation |
| Outbound HTTP | outbound | httpClient | The operation |
| Multipart upload | multipart | localUpload | The store operation |
| SSE, NDJSON, WebSocket | streamPolicy | None | None |
| Telemetry | serverTelemetry | None | None |
Server caching
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.
principalincludes the complete verified principal.tenantshares results among tenant members with identical roles, permissions and claims.publicrequires 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
maxEntriesconcurrent 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.
invalidateOnclears 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.
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:
{
"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"}
}inputFieldnames a required typed list of records. The declaration allows 1 to 64 items, 1 to 128 checked assignments and 256 KiB of serialized items. Eachitembinding 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
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.
safety | Allowed methods and retries |
|---|---|
safe | GET, HEAD |
idempotent | PUT, DELETE |
idempotencyKey | Needs a required String idempotencyField: .key, a stable key of 8 to 128 ASCII characters, and a deployment that confirms downstream support |
unsafe | One 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
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 limit | Value |
|---|---|
| File size | 16 MiB, and at most maxBytes |
| Text field | 8 KiB |
| Body fields | 16 |
| Active uploads per storage resource | 8 |
- The transport accepts one file part and the declared fields, and checks field
uniqueness and types. File bytes stream into a generated mode
0600name 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.uploadsdeclarations 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.
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:).
| Transport | Wire format |
|---|---|
| SSE | text/event-stream, comment heartbeats, optional bounded event IDs, and Last-Event-ID as input for application-defined resume (SSE specification) |
| HTTP stream | application/x-ndjson, one JSON frame per line, blank-line heartbeats that clients skip |
| WebSocket | JSON 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.jsonandaxiom.csrf.<base64url-without-padding>; onlyaxiom.jsonis 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
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.