Tutorial 1 — your first app
One command scaffolds a monorepo that already runs. No Docker daemon, no database to provision, no .env to fill in.
As of 2026-08-23, with every gate row re-measured As of 2026-09-12. This page documents main, not a published release. Every file count, gate result and command output below was re-measured on this tree: x new → bun install → the command, in a directory outside the checkout. Rows that still describe a published version say so in their own line.
Series: 1 · 2 — first feature · 3 — auth and admin · 4 — jobs and realtime · 5 — deploy free · 6 — growing up
Prerequisites
| Need | Version | Check |
|---|---|---|
| Bun | ≥ 1.4.4 | bun --version |
| Docker | only for tutorial 5 | docker --version |
| Postgres | never, locally | — |
Scaffold
cd $(mktemp -d)
bunx create-ultimate myappUnpinned on purpose. Pinning the tutorial to a version pins readers to a scaffold that stops matching this page on the next tag; README.md and llms.txt drop the pin for the same reason. To reproduce a specific release, add @<version> and read that release's tag of this page.
213 files in /tmp/tmp.XXXXXXXX/myapp
✓ created myapp — next: cd myapp && bun run setup && bun run devAs of 2026-11-03; --no-example writes 181.
Counts are a derived fact — x new --dry-run --json lists every file, and one added template moves the number.
cd myapp && bun run setupsetup complete — next: bun run devbun run setup is bun install, an .env.development.local touch, x db gen "initial", x db migrate, x db seed, then x manifest — so the database is migrated and x.manifest.json exists before the gate asks for either. x new installs nothing, so this is not optional.
x is a workspace dependency, not a global install. bunx x <command> resolves node_modules/.bin/x; the package.json scripts (bun run verify, bun run dev) resolve the same binary. A globally installed x (bun link, bun add -g) run inside the app hands over to that same file, As of 2026-09-05, and says so on stderr: a global copy carries its own @ultimat3/entity with an empty registry, and it wrote a zero-entity manifest under a green exit code before it did. ULTIMATE_KEEP_GLOBAL_CLI=1 is the opt-out — @ultimat3/cli's README has the measurement.
Two shapes, one generator
Counts are derived, never quoted here — x new --dry-run --json | jq '.data.files | length', per
shape. Measured on fresh scaffolds of this tree, As of 2026-11-05:
| Invocation | bun run setup && bun run verify on run one |
|---|---|
bunx create-ultimate myapp | green, 20 of 21 steps pass — budgets among them; roadmap skipped (a framework-repo step) |
bunx create-ultimate myapp --no-example | green, 17 of 21 steps pass — live, job, e2e skipped (no such test yet) and roadmap |
--no-example omits the post feature slice and its route. Pick it when an agent is about to write the real feature anyway — tutorial 2 starts there. Pick the default when you want a worked example to read.
The gate, first thing
bun run verifyThe app's own gate is x verify, alone. Its budgets step weighs .x/build-stats.json, and on a
fresh scaffold no such file exists yet, so the step runs x build --target static itself first
(into .x/verify-static, never over .x/static).
The default scaffold, after bun run setup, with no .x/build-stats.json beforehand,
As of 2026-11-03 (exercise measured 2026-11-05): the whole gate, in the order it runs:
✓ typecheck 3064ms
✓ lint 232ms
✓ boundaries 844ms
✓ filesize 906ms
✓ package-shape 402ms
✓ errors 951ms
✓ unit 2790ms 3 workers (budget 4.0 GB)
✓ contract 1233ms 3 workers (budget 4.0 GB)
✓ live 840ms serial
✓ job 345ms 1 of 3 workers (budget 4.0 GB)
✓ exercise 1952ms
✓ e2e 12124ms serial
✓ eval 91ms
✓ drift 1486ms
✓ contract-diff 78ms
✓ budgets 6336ms
✓ seo 40ms
✓ i18n 63ms
✓ policy 75ms
✓ manifest 1131ms
- roadmap 0ms
✓ 20 of 21 steps passed in …ms — 1 skipped: roadmap- is skipped, not passed: roadmap is a framework-repo step. The default scaffold ships its
example slice with a live test, a job test and an e2e test, so live, job and e2e are ticks on
run one; with --no-example there is no such test yet, and those three are dashes too. contract is not among them — the scaffold
ships apps/web/api/health.contract.test.ts, which is what makes it a tick on run one. Nor is
exercise: it needs no test file — it runs every action, query and job the app registers, once,
against seeded rows — and both shapes write the apps/web/exercise.ts it reads. The summary
names every skip, so a gate that is green because a suite does not exist says so on the one line you
read. Tutorial 2 turns more of those dashes into ticks.
The list is
VERIFY_STEP_NAMES (packages/cli/src/verify-step.ts), whole or not at all: the same steps run here and in the framework repo.
budgets builds before it weighs
budgets took 6.3 s above because it built first. Delete the stats and --verbose prints the line
that says so; run it again and it reads the stats it just wrote:
rm .x/build-stats.json
bunx x verify --only budgets --verbose
bunx x verify --only budgets ✓ budgets 7320ms
| built first: build stats were missing — x build --target static --out .x/verify-static (6968ms)
✓ NOT A GATE RUN — all 1 steps passed in …ms
✓ budgets 324ms
✓ NOT A GATE RUN — all 1 steps passed in …msIt builds when .x/build-stats.json is missing, written by an earlier measurement rule, or older
than the newest app source file (tests excluded), and never otherwise: run the gate again with
nothing edited and the step reads the stats it has. After an edit, the gate pays one static build.
A build that fails is the step's verdict, under the build's own code and fix:.
CI asserts the result: scaffold-smoke runs a fresh app's own scripts once, with no waiver and no fix-follow, and budgets must come back green rather than merely not-red — a skip would mean the static build bought nothing (scripts/scaffold-gate.ts).
The invariant block is typed from your columns
invariants is one callback over the whole list, and c is typed from the columns above it:
invariants: (c) => [
invariant('post_title_not_blank', c.title.trimmed().minLength(1)),
invariant('post_price_non_negative', c.price.minor.atLeast(0)),
],c.titel is a compile error that names title, not a runtime surprise. Everything x new and x g write typechecks as generated — no !, no edit before the first x verify.
What still ships broken: Known gaps.
Run it
bunx x dev--once boots, reports and exits — the shape to paste into a smoke test:
bunx x dev --once --port 3100{"ts":"2026-11-03T16:48:57.124Z","level":"info","msg":"ultimate web listening on http://localhost:3100"}
{"ts":"2026-11-03T16:48:57.118Z","level":"info","msg":"sync node ready","buildId":"e6af64a2077e5d60","path":"/_x/sync"}
{"ts":"2026-11-03T16:48:57.134Z","level":"info","msg":"jobs.worker.started","workerId":"worker-01a1a250-…","queues":["default"]}
{"ts":"2026-11-03T16:48:57.137Z","level":"info","msg":"jobs.scheduler.started","tasks":1}
{"ts":"2026-11-03T16:48:57.142Z","level":"info","msg":"live feed in-process","detail":"the embedded database has no walsender; repository writes in this process reach subscribers"}
roles web, sync, worker, scheduler
panels routes, timeline, live, jobs, db, mail, cache, policy, manifest, services, boundaries
manifest /tmp/…/myapp/x.manifest.json
introspect http://localhost:3100/_x
mcp POST /mcp
✓ dev ready on http://localhost:3100 — /_x mounted (11 panels), db=embedded events=embedded storage=embedded mail=embedded cdn=none live=in-processAn excerpt, As of 2026-11-03: the boot also logs the dev viewer warning, the metrics port and the drain --once ends with.
Four roles in one process, isolation simulated rather than skipped. replicator is opt-in via --role; migrate is not a dev role.
Why no Docker
.env.development ships every service key empty, and empty means embedded:
DATABASE_URL=
NATS_URL=
S3_ENDPOINT=
PORT=3000
ROLE=web| Key | Unset means | Set means |
|---|---|---|
DATABASE_URL | PGlite in this process, under .x/pgdata | that Postgres |
NATS_URL | in-process fanout | that NATS server |
S3_ENDPOINT | .x/storage on disk | that S3 |
Nothing in the framework branches on if (dev) — only the driver behind the seam differs. Same code path, same db(), same enqueue(). Full table: CLI reference § x dev.
What each directory is
myapp/
├── app.config.ts the one config file — names env KEYS, never values
├── apps/
│ ├── web/
│ │ ├── site/ static, 0kb JS, SEO-critical. May NOT import app/
│ │ ├── app/ authed, streaming, realtime. Feature slices live here
│ │ ├── api/ actions only — no rendering, no components
│ │ ├── shared/ actor type, tokens, primitives. A leaf both surfaces import
│ │ ├── server.ts the production entry: ROLE + PORT, nothing else
│ │ └── prerender.ts the static entry: one HTML file per `render: 'static'` route,
│ │ plus sitemap.xml and robots.txt
│ ├── admin/ the admin app, gated on `admin:read`
│ ├── desktop/ mobile/ README stubs, no code
├── packages/
│ ├── db/ schema.ts (the entity export list — the package's public
│ │ surface, not the generator's input) + migrations/
│ ├── domain/ pure types, no I/O
│ ├── i18n/ catalogs/<locale>.json — one flat file per locale
│ ├── mcp/ the app's own MCP surface over its actions
│ └── ui/ app components on semantic tokens
├── docker/ Dockerfile, .dockerignore, dev + prod compose, helm/
├── guards/ one rule per file, run by `x verify`'s boundaries step.
│ Every name in SHIPPED_GUARD_NAMES ships; `x doctor`
│ lists any missing. Delete one by deleting its file
├── bin/ setup.ts — `bun run setup`; `dev` and `verify` are x's own
└── CLAUDE.md AGENTS.md what an agent reads first: one file, AGENTS.md a symlinkBoundaries are a build error, not a note — but boundaries is a step of x verify, not a top-level command. bunx x boundaries exits X_CLI_UNKNOWN_COMMAND. Run bunx x verify and read the boundaries line. Full model: Project layout.
The database, first run
packages/db/migrations starts empty, and x db gen is its only writer. Two commands, in this order:
bunx x db gen "initial" # entities → <id>.sql, <id>.snapshot.json, <id>.hash
bunx x db migrate # applies them, then diffs the live schema against the ledger it wrotebun run setup runs both — bun install, an .env.development.local touch, x db gen "initial" when the directory holds no .sql, x db migrate, x db seed, then x manifest. x db migrate is @ultimat3/db's own migrator, the one ROLE=migrate runs, so nothing extra has to be installed.
Until the generate has run, x verify is red on its drift step — correct behaviour with a runnable fix, not a defect:
X_DB_DRIFT: schema differs from migrations
cause: packages/db has a schema but no migration recorded it
fix: x db gen "initial"That is the scaffold with the example slice, which declares one entity. x new --no-example declares none, and zero entities against zero migrations is agreement rather than drift, so its drift step is green from the first run and goes red the moment you write your first entity().
x new hand-wrote a 0000_initial.sql until 2026-08 and that was the defect — a second writer of a directory the generator owns, whose file carried no .snapshot.json, so x db migrate answered X_DB_DRIFT naming x db gen and x db gen answered X_MIGRATION_SNAPSHOT_MISSING naming version control for a file version control never had. Neither fix: now names the command that raises the other (Known gaps).
The routes are clean
x routes on a fresh scaffold reports four file routes and no findings, plus the admin's
screens. As of 2026-10, read off the templates x new writes:
| path | surface | render | hydrate | offline | declared by |
|---|---|---|---|---|---|
/ | site | static | never | precache | apps/web/site/page.tsx |
/offline | site | static | never | precache | apps/web/site/offline/page.tsx |
/dashboard | app | ssr | visible | runtime | apps/web/app/dashboard/page.tsx |
/posts | app | ssr | visible | runtime | apps/web/app/posts/page.tsx |
The admin has no page file: apps/admin/app/admin/admin.ts declares defineAdmin(), which mounts
/admin and a list, detail and form per entity. Those rows name the call and the permissions that
gate them instead of a file — defineAdmin() · admin:read + posts:read — and their --json rows
carry mount. --no-example drops /posts. x routes is not a gate step either way, so a finding
here never fails x verify — run it after scaffolding and after every x g route.
A read that should not hold up the page goes in a <Hole> on a stream page (Routes and render modes).
The five commands worth memorising
| Want | Command |
|---|---|
| is it shippable | bun run verify (add --json) |
| what is wrong with my environment | bunx x doctor --json |
| what does this error code mean | bunx x errors explain X_FORBIDDEN |
| what does this app contain | bunx x manifest --json |
| what commands exist | bunx x help — planned ones are marked (planned) and throw X_NOT_IMPLEMENTED |
Next
Tutorial 2 — your first feature: one entity, one policy, one action, one route, one test, and the five artifacts a single declaration projects.