Ajo Kit

Build and configuration

kit build, the engine descriptor, plugins and the Vite and Node APIs.

A Kit application has one production target: Ajo Engine. kit build prepares everything the engine compiler needs, checks that the server code can run there, and records what the application is allowed to touch. This page covers that build, the descriptor it writes, how to declare engine authority and plugins, and the Vite and Node APIs underneath the kit command.

kit build

The command runs Vite twice with your vite.config.ts, once for the browser and once for the server, and stages the results in .ajo/, which it clears first.

.ajo/
client/               transformed client assets and index.html
server/entry.js       generated engine entry
server/chunks/        routes, handlers, middleware and their dependencies
server/migrations/    one module per migration, plugins and project
compiler.json         the descriptor for ajo-engine-compiler

The server build bundles every dependency into one closed module graph and resolves packages through their ajo export condition, which selects the engine versions of modules such as ajo-kit/database. Kit then audits the emitted code and fails the build, listing each finding, when it contains:

  • a Node builtin, such as node:fs;
  • any other import that is neither relative nor a runtime:* engine module;
  • a dynamic import() whose argument is not a string literal;
  • a CSS or TypeScript import that survived the server build;
  • Intl APIs other than Intl.DateTimeFormat and Intl.RelativeTimeFormat, or a read of navigator.language.

Without options, kit build ends by printing the command that seals the staging tree:

Terminal
ajo-engine-compiler --input .ajo/compiler.json --output dist/ajo

With --compiler <path> it removes any previous dist/ajo and runs the given compiler executable with those arguments. The official starter keeps this as a script, where the installed binary name resolves.

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

The compiler and engine are native Linux x64 tools. Seal and run covers installing them and running dist/ajo.

The descriptor

.ajo/compiler.json is the handoff between Kit and the compiler. It uses schema 1 and lists the staged modules, the compiled migrations and client assets, and the application’s declared authority. This abridged example comes from the official starter, which uses authentication, HTTP mail and the Ajo Server plugin:

.ajo/compiler.json (abridged)
{
  "schema": 1,
  "entry": "server/entry.js",
  "modules": [
    "server/entry.js",
    "server/chunks/handler-C0AW08qE.js",
    "server/migrations/migration-0001-Bx9cAYx6.js",
    "server/migrations/migration-0007-7hM6PtWW.js"
  ],
  "client": "client",
  "migrations": [
    { "name": "plugin/ajo-kit-auth/0001_initial", "module": "server/migrations/migration-0001-Bx9cAYx6.js" },
    { "name": "project/0001_notes", "module": "server/migrations/migration-0007-7hM6PtWW.js" }
  ],
  "env": {
    "required": ["NODE_ENV", "APP_URL", "MAIL_FROM", "MAIL_TOKEN", "MAIL_URL"],
    "optional": ["APP_SECRET", "DATABASE_PATH", "TRUST_PROXY", "AJO_TIMING", "HOST", "PORT", "AJO_ORIGINS_FILE"]
  },
  "data": { "required": true },
  "fs": { "roots": ["/ajo/data", "/ajo/origin"] },
  "ipc": { "pipes": [] },
  "capabilities": ["runtime:net"]
}
FieldContents
modulesEvery emitted server module, the entry first.
clientThe client asset directory, always client.
migrationsCompiled migrations with their qualified names, in name order. Duplicate names fail the build.
envKit’s base variables followed by the sorted names you declare. The base lists are NODE_ENV and APP_URL (required) and APP_SECRET, DATABASE_PATH, TRUST_PROXY, AJO_TIMING, HOST and PORT (optional).
data.requiredTrue when the server imports ajo-kit/database, the root middleware exports bootstrap, or there are migrations. The engine then refuses to start without AJO_DATA.
fs.roots, ipc.pipesThe declared filesystem roots and named pipes, sorted.
capabilitiesruntime:net when the graph includes the HTTP transport of ajo-kit-mail; otherwise empty.

Declaring engine authority

Beyond Kit’s base variables, everything the descriptor declares about the environment, filesystem roots and pipes comes from package.json#kit.engine, in the application and in its plugins. Every field is optional, and an absent block declares nothing.

package.json
{
  "kit": {
    "engine": {
      "env": { "required": ["MAIL_FROM", "MAIL_URL", "MAIL_TOKEN"] },
      "fs": { "roots": ["/ajo/data"] }
    }
  }
}
FieldMeaning
env.requiredVariables that must be set and non-empty, or the engine refuses to start.
env.optionalVariables the application reads when they are set.
fs.rootsDirectories the engine’s filesystem and SQLite access may use. Declare /ajo/data for a database on disk.
ipc.pipesNamed pipes (FIFOs) the application may write to.

The build validates this block and fails with the name of the offending entry when:

  • it contains a key other than env, fs and ipc, or other than required/optional, roots and pipes inside them;
  • a value is not an array of non-empty strings, or repeats an entry;
  • a variable name does not match ^[A-Z_][A-Z0-9_]*$, repeats one of Kit’s base variables, or appears in both lists;
  • a path is not an absolute, normalized POSIX path: no backslashes, no . or .. segments, no repeated or trailing slash.

Declaring a root does not create it, and setting AJO_DATA grants nothing by itself: the data directory must exist and sit inside a declared root. Data on disk walks through the SQLite case.

Plugins

Kit discovers plugins among the packages your package.json lists in dependencies or devDependencies. A plugin is an installed package whose name starts with ajo- (other than ajo-kit) and whose manifest has a kit block. The installed manifest’s name must match the declared one, or Kit stops with an error.

FieldEffect
aliasAdds an @kit/<alias> import alias for the package.
serverOnlyBlocks the package from the client module graph.
migrationsA folder of migrations, stored as plugin/<package>/<name>. See sources and identities.
commandsA module exporting register(cli), which adds subcommands to kit.
engineAn env, fs and ipc block with the same shape and rules as the application’s.
node_modules/ajo-kit-auth/package.json (excerpt)
{
  "kit": { "alias": "auth", "serverOnly": true, "migrations": "./dist/migrations/" }
}
node_modules/ajo-kit-server/package.json (excerpt)
{
  "kit": {
    "serverOnly": true,
    "commands": "dist/commands.js",
    "engine": {
      "env": { "optional": ["AJO_ORIGINS_FILE"] },
      "fs": { "roots": ["/ajo/origin"] }
    }
  }
}

Each plugin’s engine block is validated on its own, and errors carry the plugin’s name. The build then combines every declaration with the application’s. Shared entries appear once, and a variable that any contributor requires is required in the descriptor even if another lists it as optional.

That is how ajo-kit-server adds kit deploy and the managed origin file. When the combined declaration names both AJO_ORIGINS_FILE and the /ajo/origin root, the build loads Kit’s reader for that file; other applications never load it. The Ajo Server host provides /ajo/origin as a read-only mount, even for an App without custom domains. To run such an artifact directly on the engine, provide the same mount.

Vite API

ajo-kit/vite exports the plugin that turns a Vite project into a Kit application.

vite.config.ts
import { defineConfig } from 'vite'
import { kit } from 'ajo-kit/vite'
import unocss from 'unocss/vite'

export default defineConfig({
  // The client entry owns a virtual route graph; keep it in Vite's plugin pipeline.
  optimizeDeps: { exclude: ['ajo-kit/client'] },
  plugins: [...kit({ css: ['virtual:uno.css'], guard: [/\/src\/database/] }), unocss()],
})

kit(options?) returns an array of plugins: the route and handler registries, the @kit aliases, the server-only guard, CSS entries and hot updates for pages and layouts.

ExportPurpose
options.guardExtra patterns for modules that must never reach the browser. Each is a RegExp, a substring, or a function of the module id. They add to the defaults, which cover every handler and wares file and every serverOnly plugin.
options.cssStyle entries imported by the client before the application hydrates.
jsx{ jsx: 'automatic', jsxImportSource: 'ajo' }, for configurations that pass JSX options through Vite’s esbuild setting.
defaultsThe CLI’s paths: ./database.sqlite, db/migrations and db/seeds.

A module named *.client.ts (or .js, .tsx, .jsx) is treated as safe for the browser and bypasses the guard patterns; its own imports are still checked. When the guard trips, the error shows the chain of imports that led there.

Node host API

ajo-kit/node exposes what the kit command uses, for test harnesses and custom scripts. Node is only the development, build and test host; there is no Node production server.

tests/server.ts
import { dev, listen } from 'ajo-kit/node'

const app = await dev({ hmr: false })
const port = await listen(app, 4173, { strict: true })
ExportBehavior
dev(options?)Creates the development server: Vite in middleware mode plus Kit’s server, rendering ./index.html (or a built-in template when it is missing). Reloads server routes when a handler, middleware, page or layout file changes. options.hmr is passed to Vite.
listen(app, port = 5173, { strict })Starts an HTTP server and resolves the port it bound. When the port is taken it tries the next one, unless strict is set, in which case it rejects.
build()Runs the engine build in the current directory and resolves { descriptor, findings, staging }.
compile(html)Returns a function that fills the <!-- ssr:name --> slots of an HTML template; missing slots become empty.

The default faces of ajo-kit/database and ajo-kit/platform are likewise Node shims for Vite, tests and CLI commands, not production runtimes.

Production environment

The engine validates its environment before it runs migrations or opens a port.

VariableMeaning
NODE_ENVMust be production.
APP_URLRequired. The public origin as an absolute http or https URL; origin(req) builds canonical links from it.
APP_SECRETRequired when the server includes ajo-kit-auth. Use a random value of at least 32 characters.
AJO_DATA, DATABASE_PATHThe data directory and a database file relative to it; see Data on disk.
HOST, PORTThe listen address, 0.0.0.0 and 8080 by default.
TRUST_PROXY1 or true to trust X-Forwarded-For and X-Forwarded-Proto from your proxy.
AJO_TIMINGAny value other than empty, 0, false or off adds Server-Timing and X-Ajo-Bytes headers and route timing logs.
AJO_ORIGINS_FILESet by the host for managed origins; it must be /ajo/origin/origins.json.

The engine checks the Host of every request before it serves assets or routes. Without managed origins the host must be APP_URL’s; anything else receives 421 Misdirected Request. Point APP_URL at the exact address clients use, including when you run the artifact locally.

Managed origins

An Ajo Server host can serve one App on several domains. It writes an ajo.origins/v1 manifest of one to nine unique HTTPS origins to /ajo/origin/origins.json and sets AJO_ORIGINS_FILE. Then APP_URL must be an exact HTTPS origin, without path, port or credentials, and must appear in that list. Requests for any listed host are admitted, and responses carry X-Ajo-Origins: v1.

Source of truth: ajo-kit README