One choice per layer. No alternatives, no adapters, no driver: config for things that are not deployment concerns.
| Layer | Decision | Rationale |
|---|---|---|
| Runtime | Bun >= 1.3 (target 2.0), only. No Node APIs unless via node: and unavoidable. |
natives replace whole dependency trees; one runtime means one perf profile to reason about |
| HTTP | thin layer over Bun.serve routes; own the lifecycle for ALS context / tracing / authz |
policy must run on every surface — impossible if a third-party router owns the request |
| DB | Postgres, no ORM — the framework owns its SQL | Postgres does queue, pubsub, vectors, logical replication. entity() is the one table declaration and postgresDriver() emits hand-written parameterised SQL, so an agent reads the statement and self-corrects |
| Validation | Standard Schema interface; our dependency-free builtin validators blessed, exposed as t |
one schema drives runtime parse + TS type + OpenAPI + MCP tool schema; Standard Schema is the seam, so ArkType/Zod/Valibot swap in behind configureSchemaProvider() with a ~40-line adapter you write — none ships |
| Auth | Better Auth, wrapped, with our policy layer on top |
sessions/OAuth/passkeys are solved; authorization is ours because it must be identical in HTTP, WS, jobs, and MCP |
| Frontend | SolidJS 2 + our own minimal router | fine-grained reactivity → streaming shells cost ~0 hydration; the router must own render mode + offline strategy, so it can't be a dependency |
| Styling | SCSS modules + design tokens, one brand seam (defineTheme()) |
build-time only, zero runtime, dark theme is a token flip. defineTheme() validates values rather than escaping them — a ; or a </style> is a refusal, not an injection. No Tailwind, no CSS-in-JS |
| Page structure | AppShell / PageHeader / Section / Toolbar from @ultimat3/ui, on top of 46 catalogued components |
a page an agent assembles from named composites is a page whose skip link, landmarks and heading order are already right |
| Jobs | Postgres queue default; redis / nats drivers behind one interface | the outbox needs the same transaction as the write — only possible in-DB |
| Realtime | 3 tiers: channels → live queries → local-first. Own protocol, Zero-shaped mutators | one mutator shape at every rung; see 03-realtime.md |
| Transport | NATS (or Redis streams) for fanout; sync nodes stateless | no sticky sessions, so sync scales on connection count alone |
| Observability | the OpenTelemetry data model, always on: spans, traceparent, counters, gauges, histograms, each behind an exporter seam |
not a flag — an agent debugging production needs traces that already exist. As of 2026-08 the shipped exporters are a no-op and an in-memory one: there is no OTLP exporter, and /metrics is a renderer that no role mounts (11-topology.md) |
| Money | integer minor units + ISO currency code, Intl.NumberFormat at the edge |
Money = { minor: number; currency: string }. Never a float |
| Time | store UTC, format with Intl.DateTimeFormat + explicit IANA tz |
a date formatted without a timeZone is a bug waiting for a user in Auckland |
| i18n | flat key catalog, loud misses (⟦key⟧), Intl for numbers/dates/money |
a missing key must be visible in dev and a x verify failure, not silently English |
Each row is a dependency subtree that never enters the lockfile.
| Bun primitive | Replaces | Deps killed (approx) |
|---|---|---|
Bun.sql |
pg, pg-pool, pg-connection-string, postgres, connection-pool wrappers |
~8 |
Bun.redis |
ioredis / redis + its command/parser packages |
~5 |
Bun.s3 |
@aws-sdk/client-s3, @aws-sdk/s3-request-presigner + the AWS SDK core chain |
~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 (15-risks.md):
| Framework primitive | Replaces | Deps killed (approx) |
|---|---|---|
@ultimat3/core image (PNG/JPEG decode, resize, encode) |
sharp + libvips native binary + imagemin plugins |
~12 |
Order of magnitude: a conventional equivalent stack is ~1,200 transitive packages; Ultimate's target is under 40 direct dependencies for the whole framework. Fewer packages is not vanity — it is fewer install failures, fewer CVE pages, and a smaller surface for an agent to misread.
Costs, stated plainly: no native-addon packages, and long-running-process maturity is less proven than Node's. See 15-risks.md.
| Excluded | Instead |
|---|---|
| GraphQL | typed action + query; OpenAPI is generated |
| Multi-runtime (Node/Deno/workerd) | Bun only |
| Multi-ORM | no ORM at all — one hand-written Postgres driver |
| Tailwind / CSS-in-JS / a second CSS system | SCSS modules + tokens |
| React Server Components | Solid stream render mode + <Suspense> |
| A plugin API before v1 | fork the blessed path; extension points earn their way in |
| Vendor edge functions, KV, image loaders | containers + our cache tiers + standard CDN headers |
| ESLint + Prettier | Biome (one binary, one config) |
| A separate migration tool | x db gen / x db migrate, drift is a x verify failure |
As of 2026-08: Bun 1.3 is the floor, Bun 2.0 the target. SolidJS is pinned to 1.9.14 — the stable line, not Solid 2. The repo previously pinned 2.0.0-experimental.16, which shipped the reactivity core and no renderer at all; Solid 2 has since moved on to 2.0.0-beta.N and split the DOM half into a separate @solidjs/web package, so that pin was stranded on an abandoned prerelease naming line. Stable is the deliberate choice: the framework publishes to npm in lockstep, and every app inherits whatever reactive core is pinned here — a beta core is a risk 28 packages hand to their users. Solid 2 is revisited when it is stable, not before. There is no ORM and no schema library to pin: the SQL driver (postgresDriver()) and the validators behind t are both ours, so a change to either is framework work by definition, never app work.