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.
| Module | Exports | Use |
|---|---|---|
runtime:http | serve, files | Serve HTTP/1.1 requests, stream server-sent events and serve static files. |
runtime:net | request | One bounded HTTP(S) request. The global fetch is built on it. |
runtime:sqlite | default (open), backup | Database connections, prepared statements and online backup. |
runtime:crypto | randomBytes, randomUUID, sha256, hmacSha256, createHash, createHmac, timingSafeEqual, argon2Hash, argon2Verify, generateKeyPair, sign, verify, validatePublicKey, ageEncryptFile | Hashing, HMAC, Argon2id, Ed25519 signatures and age encryption. |
runtime:app | default | App root and data root, arguments, environment, host name, memory and uptime, shutdown hooks. |
runtime:fs | readText, readRange, readBytes, sha256File, stat, statfs, listDirectory, makeDirectory, removeFile, watchDirectory, writeAtomic, writeBytesAtomic, writeBytesCreate, diffPages, applyPages | Confined file operations, directory watches and SQLite page deltas. |
runtime:ipc | writePipe | Atomic, 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:fsfails at load when the artifact declares no roots. Every path must be absolute, normalized and inside a declared root.runtime:sqliteopens databases only under declared roots.backupwrites a create-only copy inside a declared root.writePipeopens only an exact declared FIFO and writes one line of at mostPIPE_BUFbytes. 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
| Operation | Limit |
|---|---|
readText | 1 MiB by default, 8 MiB at most. |
readRange, readBytes | At most 8 MiB per call. |
listDirectory | Throws past 4096 entries. |
writeAtomic, writeBytesAtomic | Temporary file and rename in the same directory; mode 0600. |
writeBytesCreate | Fails if the destination exists. |
makeDirectory | One directory, mode 0700; the parent must exist. |
sign | Messages up to 8 MiB. |
request | Engine-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:
{
"name": "notes-host",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./src/index.d.ts",
"ajo": "./src/ajo.js",
"default": "./src/node.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.
| Global | Notes |
|---|---|
fetch | Buffers bounded request and response bodies. Not a streaming Fetch implementation. |
Headers, Response | Responses come from fetch, with text(), json() and arrayBuffer(). |
AbortController, AbortSignal | Including AbortSignal.timeout(). |
URL, URLSearchParams | URL parsing and query strings. |
TextEncoder, TextDecoder | UTF-8 only. The decoder has no fatal mode and no streaming decode. |
Intl | A frozen en-US and UTC subset: DateTimeFormat and RelativeTimeFormat. |
| Timers | setTimeout, setInterval, clearTimeout, clearInterval. |
performance | performance.now(), a monotonic clock. |
console | log, 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:
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.
- The artifactCapabilities, roots and evidence in the manifest.
- Security modelWhat each interface enforces, and its residuals.
- DatabaseKit’s SQLite layer on top of runtime:sqlite.
Source of truth: ajo-js docs/architecture.md and src/modules.h