Skip to content

Bare VM

github-actions[bot] edited this page Oct 2, 2026 · 4 revisions

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.

bunx create-ultimate demo --no-git
cd demo
bin/setup
bin/check
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 —
bin/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
bin/check bun. x build --target static, then x verify — the build first, because budgets weighs .x/build-stats.json and the build is that file's only writer

x new installs nothing, so bin/setup is not optional and cd demo && bin/check stops on X_BUILD_FAILED naming bun install (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), 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: 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 bin/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).

bin/check'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).

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

Wall time

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
bin/setup 6,802ms 5,135ms
bin/check 5,389ms 3,909ms
the first bin/check's verdict green, 20 of 20 steps, budgets included green, 20 of 20 steps, budgets included

The cache is the variable to watch, not the box: a cold first install takes the bun install inside bin/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
bin/setup 6,665ms 5,994ms
bin/check 7,279ms 4,886ms
the first bin/check's verdict green, 20 of 20 steps pass green, 20 of 20 steps pass

budgets is green on that first pass on every one of those runs, because bin/check builds before it verifies — 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.

Clone this wiki locally