Ajo Engine

Runtime surface

The runtime modules, web globals and the limits of each.

Inside the engine, an application sees two things: a handful of native runtime:* modules and a prelude of web-style globals. Both are small on purpose. This page lists what exists, the limits of each piece, and how the process behaves around it.

Runtime modules

The export registry lives in the engine’s src/modules.h; Kit’s runtime.d.ts holds the matching type declarations. An import succeeds only when the module appears in the artifact’s capabilities. Otherwise it fails with an undeclared-capability error.

ModuleExportsUse
runtime:httpserve, filesServe HTTP/1.1 requests, stream server-sent events and serve static files.
runtime:netrequestOne bounded HTTP(S) request. The global fetch is built on it.
runtime:sqlitedefault (open), backupDatabase connections, prepared statements and online backup.
runtime:cryptorandomBytes, randomUUID, sha256, hmacSha256, createHash, createHmac, timingSafeEqual, argon2Hash, argon2Verify, generateKeyPair, sign, verify, validatePublicKey, ageEncryptFileHashing, HMAC, Argon2id, Ed25519 signatures and age encryption.
runtime:appdefaultApp root and data root, arguments, environment, host name, memory and uptime, shutdown hooks.
runtime:fsreadText, readRange, readBytes, sha256File, stat, statfs, listDirectory, makeDirectory, removeFile, watchDirectory, writeAtomic, writeBytesAtomic, writeBytesCreate, diffPages, applyPagesConfined file operations, directory watches and SQLite page deltas.
runtime:ipcwritePipeAtomic, nonblocking writes to exact declared FIFOs. Never process execution.

Declared authority

Importing a module is only the first check. Paths and pipes are authorized separately, from the artifact’s fs.roots and ipc.pipes:

  • runtime:fs fails at load when the artifact declares no roots. Every path must be absolute, normalized and inside a declared root.
  • runtime:sqlite opens databases only under declared roots. backup writes a create-only copy inside a declared root.
  • writePipe opens only an exact declared FIFO and writes one line of at most PIPE_BUF bytes. A missing pipe, a pipe without a reader and a full pipe are distinct, retryable errors.
  • Static file serving may also read the App’s own artifact directory. That does not make the directory a general filesystem or SQLite root.

Bounds worth knowing

OperationLimit
readText1 MiB by default, 8 MiB at most.
readRange, readBytesAt most 8 MiB per call.
listDirectoryThrows past 4096 entries.
writeAtomic, writeBytesAtomicTemporary file and rename in the same directory; mode 0600.
writeBytesCreateFails if the destination exists.
makeDirectoryOne directory, mode 0700; the parent must exist.
signMessages up to 8 MiB.
requestEngine-owned framing headers; global-unicast destinations only; the whole response is buffered.

When you need a runtime module

Node has no runtime:* modules, so code that imports them needs a separate implementation for kit dev and tests. Kit’s engine build resolves package exports with the ajo condition, which lets a small private package choose its implementation at build time. Ajo Server’s administration App reaches the engine this way:

packages/notes-host/package.json
{
  "name": "notes-host",
  "private": true,
  "type": "module",
  "exports": {
    ".": {
      "types": "./src/index.d.ts",
      "ajo": "./src/ajo.js",
      "default": "./src/node.js"
    }
  }
}
packages/notes-host/src/ajo.js
import app from 'runtime:app'

export const uptimeSeconds = app.uptimeSeconds
export const rssBytes = app.rssBytes
export const onShutdown = callback => app.onShutdown(callback)

Production never falls back to the Node implementation. Keep both sides aligned where they share a contract; the declaration file alone does not prove that they do.

Web-style globals

The prelude is plain JavaScript compiled into the runtime. It provides a deliberate subset of web APIs, not browser or Node parity. Beyond the language built-ins and this list, nothing is defined: there is no process, Buffer, Blob, FormData, stream API or global crypto.

GlobalNotes
fetchBuffers bounded request and response bodies. Not a streaming Fetch implementation.
Headers, ResponseResponses come from fetch, with text(), json() and arrayBuffer().
AbortController, AbortSignalIncluding AbortSignal.timeout().
URL, URLSearchParamsURL parsing and query strings.
TextEncoder, TextDecoderUTF-8 only. The decoder has no fatal mode and no streaming decode.
IntlA frozen en-US and UTC subset: DateTimeFormat and RelativeTimeFormat.
TimerssetTimeout, setInterval, clearTimeout, clearInterval.
performanceperformance.now(), a monotonic clock.
consolelog, info and debug to standard output; warn and error to standard error.

fetch

Request bodies are strings or Uint8Array; the whole response body is buffered up to a bounded size before fetch resolves. Every connection and every redirect hop is checked after name resolution: destinations that are not global, such as loopback or private addresses, are refused. TLS peers are verified against the configured CA store, so an image that makes outbound HTTPS requests needs a CA bundle.

Intl

Intl is not ECMA-402. DateTimeFormat and RelativeTimeFormat format in en-US and UTC only, alongside the Date locale methods. An unsupported locale, option, time zone or unit throws instead of approximating; even a timeZone option is rejected, and relative units stop at days. Kit’s build rejects server code that uses other Intl members or navigator.language. A journal App formats its dates within those bounds:

TypeScript
const format = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
format.format(Date.UTC(2026, 8, 25)) // 'Sep 25, 2026'

console

Each argument is converted to a string and separated by a space; objects are not inspected. Log with JSON.stringify(value) when you need structure.

The process

One JavaScript thread. JavaScript runs on a single thread, driven by an epoll reactor that dispatches I/O, timers and worker completions. DNS resolution and Argon2 run on a native worker pool, and worker jobs never touch JavaScript values. Ordinary SQLite work and signature verification stay synchronous on the JavaScript thread, so a slow query delays every request.

Unhandled rejections are fatal. A promise rejection that nothing handles ends the process. Await or catch every promise you start, including fire-and-forget work.

Shutdown is bounded. SIGTERM or SIGINT starts a drain: the engine stops accepting new work, runs the registered shutdown hooks and waits for outstanding work under a deadline. If the deadline passes, the process exits with an error. Native deadlines and quotas use the reactor’s own timers, not writable JavaScript globals.

The language profile

The runtime is parserless: eval and the Function constructor are never admitted, and WeakRef and FinalizationRegistry are outside the current profile. The compiler records these uses in the artifact’s feature evidence, and the runtime refuses an artifact that requires them. The profile is not all of ECMAScript plus browser APIs; unsupported surfaces stay explicit rather than approximated.

Source of truth: ajo-js docs/architecture.md and src/modules.h