Installation
Prerequisites
| Requirement | Version | Needed for | Notes |
|---|---|---|---|
| Bun | >= 1.4.4 (engines.bun; target 2.0) | everything | the only runtime. No Node, Deno, or workerd support — ever |
| Postgres | 16+ | prod, parity checks | optional in dev — x dev embeds one in .x/pg |
| Docker | any | parity checks, prod images | never required for x dev; x build --target docker needs it |
| Redis | 7+ | cache tier 3 in prod | dev uses an in-process map behind the same interface |
| NATS | 2.10+ | fanout in prod | dev runs it in-process |
| Native toolchain | — | nothing | no native addons in the dependency graph; no node-gyp, no sharp, no libvips |
Bun below the floor fails before anything else runs:
X_BUN_VERSION: bun version is below the framework floor
cause: Bun 1.4.3 is older than the required 1.4.4
fix: bun upgradex doctor --json checks the runtime, the embedded Postgres, ports, and the resolved env. Outside an app, x is on no PATH and bunx x fetches an unrelated npm package named x: spell it bunx @ultimat3/cli doctor --json there, and start an app with bunx create-ultimate myapp.
| Platform | Status | Proved by |
|---|---|---|
| Linux x64 | supported | every CI job; scaffold-smoke gates a fresh scaffold twice per shape |
| macOS | supported | the macos CI job: a fresh scaffold's bun run setup and its gate on macos-latest |
| Windows, native (PowerShell, cmd) | supported | the windows CI job: lint, typecheck, a path-heavy unit subset and a scaffold's gate, in pwsh |
| Without Docker | Works |
|---|---|
bun run dev, bun run setup, bun run verify (the gate) | yes — the database is embedded and no service is dialled (Bare VM) |
| a deploy | takes an image (x build --target docker needs Docker, or a host that builds the Dockerfile — Deploy recipes), or x build --target binary with no container runtime |
Create an app
bunx create-ultimate myapp && cd myapp && bun run setup && bun run devcreate-ultimate is a thin front for x new. Inside an existing workspace, use x new directly.
PowerShell and cmd run the same commands. bun run setup runs the app's bin/setup.ts —
TypeScript that Bun executes, no bash and no WSL — and bun run dev and bun run verify are
x dev and x verify. Windows PowerShell 5.1 has no &&: type the commands one per line there
(PowerShell 7 and cmd take the line as written).
bun run setup is not optional. x new writes files and installs nothing, so cd myapp && x dev
stops on X_BUILD_FAILED naming bun install (measured As of 2026-08-23; this page said
otherwise until then). bun run setup is six steps, idempotent and safe to re-run:
| # | Step | Why it is in the script |
|---|---|---|
| 1 | bun install | x new writes files and installs nothing |
| 2 | touch .env.development.local | per-box secrets, gitignored, wins over the committed .env.development |
| 3 | x db gen "initial" | skipped when packages/db/migrations already holds a .sql — x db gen is that directory's only writer |
| 4 | x db migrate | applies them, then diffs the live schema against the ledger |
| 5 | x db seed | run through the CLI, never bun run: the CLI owns the connection, so it reaches the same embedded database the migration just wrote to |
| 6 | x manifest | x.manifest.json is a projection of the loaded app, so x new cannot write it. Nothing else ever ran the command, and x verify's manifest step now refuses its absence with X_MANIFEST_MISSING |
Every flag, from the registry — bun run x -- help new, or x help new in an app. There are six,
each with a default, so x new asks nothing: an agent cannot answer a prompt and is never given
one.
| Flag | Default | Effect |
|---|---|---|
--dir <path> | cwd | parent directory the app is written into |
--example / --no-example | --example | include the example feature slice; --no-example gives the same shape with an empty app/ |
--shape <shape> | none | start from api, blog or dashboard instead of the dashboard and example slice — what each writes and leaves out: Shapes |
--git / --no-git | --git | git init and commit the scaffold |
--dry-run | off | print the file list, write nothing |
--force | off | write into a directory that already exists |
--json | off | machine-readable result: the directory, every path written, the git outcome |
There is no --admin, --locale, --tz, --currency, --no-install or --yes — this table
listed all six until 2026-08-23 and the parser refuses each with X_CLI_BAD_FLAG. apps/admin/ is
always written; locale, timezone and currency are app.config.ts fields you edit after scaffolding
(Configuration).
What x new writes, and who owns it afterwards:
| Artifact | Author | Rule |
|---|---|---|
.env.development | generated, complete and valid, committed | non-secret defaults; per-box secrets go in .env.development.local, which wins. Dev secrets carry a loud dev-only- prefix. The scaffold writes no .env — Bun still loads .env, .env.<mode> and .env.local when they exist (packages/core/src/env-example.ts:31), and .env is the one you never commit |
.env.example | generated from envSchema, plus the framework's deploy-required secrets | never hand-edited — it is a projection of the declaration, and drift fails x verify. Regenerate with x env example |
app.config.ts | yours | the one config file; validated at boot (Configuration) |
x.manifest.json | generated every build | routes, entities, actions, jobs, policies, tags, MCP tools, budgets. Never hand-edited; drift fails x verify |
openapi.json | generated | HTTP surface from action / query declarations |
CLAUDE.md, and AGENTS.md a symlink to it | human-authored stub, one file | short, terse; a copy only where Windows refuses the link. Ultimate never generates prose docs at runtime |
.github/workflows/ci.yml | generated | bun run setup then bun run verify, on push and pull request — yours to edit, and the same two commands the app's README.md opens with |
docker/ | generated | dev compose + per-role prod compose + Dockerfile (Deployment) |
packages/db/migrations/ | not written at all | x db gen is its only writer, As of 2026-08. First commands in a new app: x db gen "initial", then x db migrate — bun run setup runs both. Until the first one has, x verify's drift step is red and names it, for any app declaring an entity; --no-example declares none, and zero against zero is agreement |
Typed env, validated at boot
Declared at module scope in app.config.ts, next to defineConfig. There is no env.ts — app.config.ts is the one file the CLI and the runtime both load, so it is the one place the env gate can run before anything binds.
// app.config.ts
import { defineConfig, defineEnv } from '@ultimat3/core';
export const env = defineEnv({
DATABASE_URL: { type: 'url' },
NATS_URL: { type: 'url', required: false },
S3_BUCKET: { type: 'string' },
VAPID: { type: 'string', required: false },
STRIPE_KEY: { type: 'string', pattern: /^sk_/, secret: true },
});
export const config = defineConfig({ name: 'myapp' /* … */ });Boot parses this before any listener binds. Failure costs ~40ms and exit 1 — not a 3am undefined in a payment handler.
X_ENV_MISSING: required environment variables are missing or invalid
cause: STRIPE_KEY missing; NATS_URL is not a URL ("nats:4222")
fix: add STRIPE_KEY, NATS_URL to .env (copy .env.example), then run: x env checkThe fix: is per-key and built from your own declarations (packages/core/src/env.ts:237); defineEnv({ KEY: { fix } }) overrides it for one key.
| Property | Behavior |
|---|---|
| Access | env.STRIPE_KEY is typed. A process.env read outside the schema is a lint error |
| Per-role | a role requires only the keys it uses — ROLE=worker does not fail on a missing VAPID |
| Repair | x env example regenerates .env.example from the schema; copy the keys it names into .env.development.local in dev, or into your platform's secret store in production. There is no x env --fix — x env declares check and example and no flags, so x env check --fix dies at the parser with X_CLI_BAD_FLAG |
| CI | x verify's manifest step holds .env.example to the schema's projection (X_ENV_EXAMPLE_DRIFT); x env check is not a gate step — run it against the deploy's own environment (ULTIMATE_ENV=production x env check), where it also reports the framework's deploy-required secrets |
| Provenance | /_x → Env shows every resolved key and which source it came from |
What Bun natives replace
Each row is a dependency subtree that never enters the lockfile. Target: under 40 direct dependencies for the whole framework.
| Bun primitive | Replaces | Deps killed (approx) |
|---|---|---|
Bun.sql | pg, pg-pool, pg-connection-string, postgres, pool wrappers | ~8 |
Bun.redis | ioredis / redis + command/parser packages | ~5 |
Bun.s3 | @aws-sdk/client-s3, @aws-sdk/s3-request-presigner + AWS SDK core | ~25 |
Bun.serve WebSockets | ws, socket.io, engine.io, uWebSockets.js | ~10 |
bun test | vitest / jest, @types/jest, coverage + mock + snapshot plugins | ~30 |
Bun.build | esbuild / rollup / vite + framework plugin + postcss chain | ~40 |
Bun.Transpiler / macros | ts-node, tsx, swc, babel presets | ~15 |
Bun.password | bcrypt / argon2 native addons | ~4 |
Bun.file / Bun.write | fs-extra, graceful-fs, globby | ~6 |
bun --hot | nodemon, concurrently, HMR middleware | ~5 |
One more subtree dies with no Bun native behind it — a pure-TypeScript framework pipeline, written because native addons are blocked:
| Framework primitive | Replaces | Deps killed (approx) |
|---|---|---|
@ultimat3/core image | sharp + libvips + imagemin plugins | ~12 |
Stated cost: no native-addon packages, and Bun's long-running-process maturity is less proven than Node's.
Editor and agent setup
Two MCP servers, and an app ships both. The dev tools are x mcp serve — x dev does not
start them, and nothing listens on 9229 until this command runs. A new app's .mcp.json already
names the stdio form, as bunx @ultimat3/cli mcp serve --transport stdio, so a client that reads
that file starts it with no setup; MCP and AI says what to give one that does not.
The app's own tools are POST /mcp on the app (apps/web/mcp.ts), mounted by x dev and the
web role, refusing any request without a bearer token. The HTTP form below prints its token:
bunx x mcp serve --transport stdio # a client that spawns the server
bunx x mcp serve --transport http --port 9229 # POST /mcp; the bearer token is printed at bootstdio and http are the two transports (x help mcp) — there is no ws transport, and this page
told readers to configure one until 2026-08-23. Register the URL and the printed token with whatever
MCP client you use; a request without the token is answered 401 with no catalog.
The catalog is bunx x mcp tools --json: every tool's exact name and description, read off the
server itself rather than counted here. The core of it:
| Tool | Replaces the agent's usual guess |
|---|---|
routes.list | grepping a router directory |
schema.describe | reading migration files in order |
policies.list | "is this endpoint protected?" |
actions.describe | reading api/ by hand — actions and queries in one call |
jobs.inspect | reading jobs.ts for retry policy and step names |
queue.depth | guessing whether the worker is keeping up |
manifest.read | ten separate reads |
errors.explain | a web search for an error string |
db.query | inventing a query and hoping (read-only; 100-row default, 1000-row maximum) |
db.migrate | mutating the dev DB — writes land in a branch DB only |
tests.run | parsing terminal output |
verify.run | guessing whether the work is shippable |
logs.tail | scrollback archaeology |
Read tools are unrestricted in dev; write tools are scoped to branch environments. The dev server is never exposed under ROLE=web. db.query refuses a batch, a write keyword anywhere at statement level (a data-modifying CTE included), a locking clause, EXPLAIN ANALYZE, and the banned function families matched by prefix (pg_read_*, pg_ls_*, lo_*, pg_advisory_*, pg_sleep*, set_config, …) — X_MCP_QUERY_REJECTED, before the host sees the string. Use x mcp for a standalone server (CI, remote agents). Editor config: Biome is the only formatter/linter — one binary, one config, no ESLint or Prettier.
Upgrading and removal
| Task | Command |
|---|---|
| Framework version bump | x upgrade --json, one major per run (CLI reference). An app below 30.0.0 has no such command yet: bunx @ultimat3/cli@latest upgrade --json |
| Check what a bump would change | x upgrade --dry-run --json: the pins, the Bun floor edits and the section to read. Writes nothing, runs no gate |
| Remove dev state (embedded PG, storage, caches) | rm -rf .x |
| Remove the CLI | it ships with the app; deleting the repo is the uninstall |
Framework ranges: an app declares every @ultimat3/* package at ^<version>, as x new writes it, and bun.lock pins the exact build. Keep them on one version: the packages move in lockstep, and x upgrade moves them together. A third-party wrapper declares them as peerDependencies at ^<major> (Publish it).
Version pinning: As of 2026-11, the Bun floor is the one stated at the top of this page and 2.0 the target; SolidJS 2 is in beta and is pinned exactly, and its upgrade is framework work, not app work. There is no ArkType or Drizzle pin to carry — @ultimat3/schema ships its own dependency-free validators and @ultimat3/entity its own postgresDriver(). Details and codemod inventory in Upgrading. Boot failures and port conflicts in Troubleshooting.