# Bare VM

A fresh Ubuntu box with **bun** and **git** runs a scaffolded app's whole gate. No Docker daemon, no
Postgres, no Redis, no NATS, no object store, no `.env` editing. Bun runs all four commands below;
git is what commits the scaffold, and `--no-git` is the form that skips it.

```sh
bunx create-ultimate demo --no-git
cd demo
bun run setup
bun run verify
```

| Command | What it needs from the box |
|---|---|
| `bunx create-ultimate demo --no-git` | bun, and a network the npm registry answers on. `--no-git` skips `git init`, which is the form an automated provisioner uses; without it the scaffold is committed, and that is git's only job here |
| `cd demo` | — |
| `bun run setup` | bun. Six steps: `bun install`, an `.env.development.local` touch, `x db gen "initial"` when `packages/db/migrations` holds no `.sql`, `x db migrate`, `x db seed`, `x manifest` |
| `bun run verify` | bun. `x verify`, the whole gate: its `budgets` step runs `x build --target static` itself, because `.x/build-stats.json` does not exist yet on a fresh box (`As of 30.0`) |

`x new` installs nothing, so `bun run setup` is not optional and `cd demo && bun run verify` stops on
`X_BUILD_FAILED` naming `bun install` ([Installation](/installation)).

## Why no service is needed

The scaffold's `.env.development` ships committed non-secret defaults, and it leaves `DATABASE_URL`
empty. An empty `DATABASE_URL` is not a hole to fill — it is the switch that selects the embedded
database, in `resolveServices`
([`packages/cli/src/runtime-bindings.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/cli/src/runtime-bindings.ts)),
which is the one place the three service bindings are decided:

| Binding | Unset env | Resolves to | Set it to |
|---|---|---|---|
| db | `DATABASE_URL` | `pglite://<app>/.x/pgdata` — Postgres compiled to WASM, running **inside this process** | a `postgres:` URL |
| events | `NATS_URL` | `inproc://events` — in-process fanout | a NATS URL |
| storage | `S3_ENDPOINT` | `file://<app>/.x/storage` | an S3 endpoint |

The database is
[`packages/db/src/pglite.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/db/src/pglite.ts):
a real Postgres, WASM-compiled, opened on a directory under `.x/`, so `x db migrate` and `x db seed`
write to the same engine the app then reads — no container to wait for, no port to publish, and a
restart keeps the data. The module is an **optional peer resolved at first query, never at import**,
so an image that only ever talks to a managed Postgres does not carry the WASM it will never load.
`@electric-sql/pglite` is a `devDependencies` entry of the generated app, which is what puts it on
the box during step 1 of `bun run setup`.

**Its absence is a coded refusal, never a silent skip**: `X_DB_UNAVAILABLE`, whose `fix:` is
`bun add @electric-sql/pglite, or set DATABASE_URL to a Postgres server and re-run` — a command the
reader runs, on a box that has just told them what is wrong.

`x doctor` says it earlier than the first query does. Its external-database probe answers nothing
where `DATABASE_URL` is unset, and that silence is exactly this box, so it asks the embedded
question too: does the peer **resolve** from the app root — a resolve, never an import, since
loading it boots the WASM and takes the single-writer lock the next command needs. Red only where
`DATABASE_URL` is unset *and* the peer is unresolvable ([CLI reference](/cli-reference)).

The gate's `live`, `job` and `e2e` steps need no service of their own in a generated app: the
queue is Postgres (`SELECT … FOR UPDATE SKIP LOCKED`), the fanout is in-process, and both resolve
through the table above.

## What the embedded database does not do

**No logical replication.** PGlite has no walsender, so there is no write-ahead log to decode and no
slot to take. A `--live` query still works: `x dev` installs the in-process bridge instead — the
same row observer the framework's own live tests run on — and the boot line says which feed you
got, `live=in-process` under the embedded database and `live=replication` under a real one
(`packages/cli/src/runtime-live-feed.ts`). Its honest bound is that a write made by **another process**
is invisible to it, which holds by construction under `x dev`, where every role is this one process
([Realtime](/realtime)).

Reach for the dev compose file only when you want parity against a real Postgres, NATS and S3
([Deployment](/deployment)). Nothing on this page uses it.

## Wall time

Both tables predate 30.0 and time `bun run check`, the scaffold's old wrapper: `x build --target
static`, then `x verify`. `bun run verify` does the same work in one command, the build inside
`budgets`, so the figures are its order of magnitude, not its measurement.

On a **WSL2 developer box — not a bare VM, and not a CI runner** — on a Bun the CLI's own floor
accepts (`x doctor`), against real `x new` scaffolds, first pass, nothing waived and no fix-follow,
warm Bun cache, `As of 2026-09-11`:

| Measure | Default scaffold | `--no-example` |
|---|---|---|
| files written | 161 | 133 |
| `bun run setup` | **6,802ms** | **5,135ms** |
| `bun run check` | **5,389ms** | **3,909ms** |
| the first `bun run check`'s verdict | **green, every step**, `budgets` included | **green, every step**, `budgets` included |

The cache is the variable to watch, not the box: a **cold** first install takes the `bun install`
inside `bun run setup` to **12.0s** on the same machine.

And on a **free `ubuntu-latest` runner**, which is the measurement that speaks for a provisioned
box — `ci.yml`'s `scaffold-smoke` job, running these same two scripts on a scaffold written outside
the checkout, `As of 2026-09-12`:

| Measure | Default scaffold | `--no-example` |
|---|---|---|
| files written | 161 | 133 |
| `bun run setup` | **6,665ms** | **5,994ms** |
| `bun run check` | **7,279ms** | **4,886ms** |
| the first `bun run check`'s verdict | **green, every step passes** | **green, every step passes** |

On both dates the gate was 20 steps; `exercise` joined it in 2026-11, so a run today counts 21.

`budgets` is green on that first pass on every one of those runs, because the gate builds before
it weighs — nothing is waived to get there, and no printed `fix:` is followed before the verdict
is taken. The job prints the table itself, one row per command and one per gate step, so the run is
its own measurement rather than a number this page asserts: read it from the job's log when you want
today's.
