# 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](/tutorial-02-first-feature) · [3 — auth and admin](/tutorial-03-auth-and-admin) · [4 — jobs and realtime](/tutorial-04-jobs-and-realtime) · [5 — deploy free](/tutorial-05-deploy-free) · [6 — growing up](/tutorial-06-growing-up)

## Prerequisites

| Need | Version | Check |
|---|---|---|
| Bun | ≥ 1.4.4 | `bun --version` |
| Docker | only for [tutorial 5](/tutorial-05-deploy-free) | `docker --version` |
| Postgres | never, locally | — |

## Scaffold

```bash
cd $(mktemp -d)
bunx create-ultimate myapp
```

**Unpinned 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.

```text
  213 files in /tmp/tmp.XXXXXXXX/myapp
✓ created myapp — next: cd myapp && bun run setup && bun run dev
```

`As 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.

```bash
cd myapp && bun run setup
```

```text
setup complete — next: bun run dev
```

`bun 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](https://github.com/developerz-ai/ultimate/blob/main/packages/cli/README.md) 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](/tutorial-02-first-feature) starts there. Pick the default when you want a worked example to read.

## The gate, first thing

```bash
bun run verify
```

The 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:

```text
  ✓ 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](/tutorial-02-first-feature) turns more of those dashes into ticks.

The list is
`VERIFY_STEP_NAMES` ([`packages/cli/src/verify-step.ts`](https://github.com/developerz-ai/ultimate/blob/main/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:

```bash
rm .x/build-stats.json
bunx x verify --only budgets --verbose
bunx x verify --only budgets
```

```text
  ✓ 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 …ms
```

It 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`](https://github.com/developerz-ai/ultimate/blob/main/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:

```ts
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](/known-gaps).

## Run it

```bash
bunx x dev
```

`--once` boots, reports and exits — the shape to paste into a smoke test:

```bash
bunx x dev --once --port 3100
```

```text
{"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-process
```

An 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:

```bash
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](/cli-reference).

## 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 symlink
```

Boundaries 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](/project-layout).

### The database, first run

`packages/db/migrations` starts **empty**, and `x db gen` is its only writer. Two commands, in this order:

```bash
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 wrote
```

`bun 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:

```text
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](/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](/routes-and-render-modes#what-a-hole-does)).

## 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](/tutorial-02-first-feature): one `entity`, one `policy`, one `action`, one `route`, one test, and the five artifacts a single declaration projects.
