Ajo Engine

Seal and run

Install the native pair, seal a Kit build and run it.

This page takes a Kit application, here a small notes App, from kit build to a running engine process. Sealing and running happen on a verified Linux x64 host. UI development can happen elsewhere; only these steps need the native tools.

Install the native pair

Install the runtime and the compiler together, at the same exact version. They are development dependencies: the compiler runs on the build host, and the runtime binary is later copied into the App’s image.

Terminal
npm install --save-dev --save-exact ajo-engine@0.1.0 ajo-engine-compiler@0.1.0

With pnpm:

Terminal
pnpm add -D --save-exact ajo-engine@0.1.0 ajo-engine-compiler@0.1.0

npm rejects a host outside the packages’ declared OS and CPU. pnpm’s default configuration can install an incompatible package; enable engine-strict so it refuses as well:

.npmrc
engine-strict=true

Installation never compiles or downloads anything. Neither package has lifecycle scripts, a JavaScript launcher, a downloader or runtime npm dependencies. Each contains one executable, its PIN, checksums, provenance record, the ISC license and the third-party notices. Keep the lockfile from this installation so later builds use the same pair.

Keep the toolchain separate

If the application is developed on macOS or Windows, do not make the native pair a mandatory dependency of the project: no support is claimed there, and npm rejects the installation. Install it instead in its own directory on a verified Linux host, such as a dedicated Linux environment or virtual machine, and point Kit at it. This is how this website is built.

Terminal
mkdir -p /path/to/ajo-toolchain
cd /path/to/ajo-toolchain
npm install --save-dev --save-exact ajo-engine@0.1.0 ajo-engine-compiler@0.1.0

The executables are then at /path/to/ajo-toolchain/node_modules/.bin/ajo-engine-compiler and /path/to/ajo-toolchain/node_modules/.bin/ajo-engine. The application’s own dependencies stay portable.

Build and seal

kit build has a single target, the engine. It writes a staging tree to .ajo/: the transformed client, the closed server graph, the compiled migration registry and the compiler.json descriptor. Every build rejects Node builtins and other imports that would break the engine’s closed module graph. Without a compiler, it stops there and prints the command to run next:

Terminal
pnpm exec kit build
Output
ajo-engine-compiler --input .ajo/compiler.json --output dist/ajo

With --compiler, Kit runs that command itself. It removes any previous dist/ajo and seals the staging tree into a fresh one:

Terminal
pnpm exec kit build --compiler /path/to/ajo-toolchain/node_modules/.bin/ajo-engine-compiler

When the compiler is installed in the project, package scripts can name it directly, because they see node_modules/.bin on their path:

package.json
{
  "scripts": {
    "build": "kit build",
    "artifact": "kit build --compiler ajo-engine-compiler"
  }
}

You can also run the compiler on the staged descriptor yourself. Give it an output path that does not exist yet:

Terminal
pnpm exec kit build
npx --no-install ajo-engine-compiler --input .ajo/compiler.json --output .ajo/artifact

The descriptor is the handoff contract between Kit and the compiler: the module graph, the client assets, the compiled migrations and the App’s declared authority. The artifact explains what the compiler does with it.

Debug seals

Source text is stripped by default. Production chunks keep function names and line and column tables, so stack traces stay named and located, but carry no source. --keep-source produces a source-bearing debug seal:

Terminal
npx --no-install ajo-engine-compiler --keep-source --input .ajo/compiler.json --output .ajo/debug

Run the artifact

ajo-engine takes the artifact directory. Any further arguments are passed to the application. The process reads its configuration from the environment:

Terminal
NODE_ENV=production HOST=127.0.0.1 PORT=8080 APP_URL=http://localhost:8080 \
  /path/to/ajo-toolchain/node_modules/.bin/ajo-engine dist/ajo

Kit declares NODE_ENV and APP_URL as required in every descriptor, so the engine refuses to start without them. Kit then validates the values before it starts serving.

VariableRequiredMeaning
NODE_ENVYesMust be production; Kit refuses any other value on the engine.
APP_URLYesAn absolute http or https URL; outside local runs, the App’s public origin. With a host-managed origins manifest, an exact HTTPS origin listed there.
HOSTNoListen address, a hostname or numeric IP. Defaults to 0.0.0.0.
PORTNoAn integer from 1 to 65535. Defaults to 8080.
APP_SECRETWith ajo-kit-authA strong random secret; production refuses a missing, short or placeholder value.
DATABASE_PATHNoA relative file name inside the data root, or :memory:. Defaults to ./database.sqlite.
AJO_DATAWhen data is requiredAn existing directory inside a declared filesystem root.

App environment

An App declares its own variables in package.json, under kit.engine.env. Names use uppercase letters, digits and underscores. A required name that is missing at startup stops the engine before any application code runs.

package.json
{
  "kit": {
    "engine": {
      "env": { "required": ["MAIL_URL", "MAIL_TOKEN"], "optional": ["MAIL_FROM"] }
    }
  }
}

Refusals are printed on standard error and name what is missing:

Text
[ajo] [error] artifact requires environment variable 'MAIL_URL'

The data root

An App that stores SQLite on disk needs a writable data root. Declare it as a filesystem root, then point AJO_DATA at it when you run the engine. Setting AJO_DATA alone grants nothing: the path must lie inside a declared root.

package.json
{
  "kit": {
    "engine": {
      "fs": { "roots": ["/ajo/data"] }
    }
  }
}

Kit marks the data root as required when the App uses its database. The engine then refuses to start unless AJO_DATA names an existing directory, and every declared root must be valid and available at startup. Database paths resolve beneath the data root; absolute paths and .. segments are rejected. Kit applies the App’s compiled migrations when it starts, before it listens.

Terminal
NODE_ENV=production APP_URL=http://localhost:8080 \
AJO_DATA=/ajo/data DATABASE_PATH=notes.sqlite \
  /path/to/ajo-toolchain/node_modules/.bin/ajo-engine dist/ajo

Before you package it

  • The runtime and compiler come from the same release; the artifact’s pin matches the runtime’s.
  • The artifact was sealed without --keep-source.
  • Every required variable, the data root and each declared root exist where the App will run.
  • The runtime’s ISC license and third-party notices travel with any image you redistribute. Ajo Server’s image guide shows one way to include them.

Source of truth: ajo-engine README.md and ajo-kit README.md