Here's the full flow, start to finish:
---
1. Startup (index.ts → commands/dev/index.ts)
Running bun ../../index.ts dev:
- init() is called twice (once in index.ts, once in the dev command — redundant but harmless). It ensures all required
directories exist (src/pages/, .bunext/client/hydration-src/, public/pages/, etc.) and creates a blank bunext.config.ts
if missing.
- global.CONFIG is set with development: true.
- global.ROUTER is created as a Bun.FileSystemRouter pointing at src/pages/ using Next.js-style routing.
---
2. Initial Build (start-server.ts → allPagesBundler)
Before accepting requests, allPagesBundler() runs:
1. grabAllPages({ exclude_api: true }) — recursively walks src/pages/, skipping api/ routes and directories with ( or )
in the name, returning an array of { local_path, url_path, file_name }.
2. For each page, writeWebPageHydrationScript() generates a .tsx entrypoint in
.bunext/client/hydration-src/pageName.tsx. That file looks like:
import React from "react";
import { hydrateRoot } from "react-dom/client";
import App from "/absolute/path/to/src/pages/index.tsx";
const container = document.getElementById("bunext-root");
hydrateRoot(container, );
3. Stale hydration files (for deleted pages) are cleaned up.
4. bundle() runs bun build .bunext/client/hydration-src/\*.tsx --outdir public/pages/ --minify via execSync. Bun bundles
each entrypoint for the browser, outputting public/pages/pageName.js (and public/pages/pageName.css if any CSS was
imported).
---
3. Server Start (server-params-gen.ts)
Bun.serve() is called with a single fetch handler that routes by URL pathname:
┌─────────────────┬──────────────────────────┐
│ Path │ Handler │
├─────────────────┼──────────────────────────┤
│ /\_\_hmr │ SSE stream for HMR │
├─────────────────┼──────────────────────────┤
│ /api/_ │ handleRoutes │
├─────────────────┼──────────────────────────┤
│ /public/_ │ Static file from public/ │
├─────────────────┼──────────────────────────┤
│ /favicon.\* │ Static file from public/ │
├─────────────────┼──────────────────────────┤
│ Everything else │ handleWebPages (SSR) │
└─────────────────┴──────────────────────────┘
---
4. Incoming Page Request → handleWebPages
4a. Route matching (grab-page-component.tsx)
- A new Bun.FileSystemRouter is created from src/pages/ (in dev; in prod it uses the cached global.ROUTER).
- router.match(url.pathname) resolves the URL to an absolute file path (e.g. / → .../src/pages/index.tsx).
- grabRouteParams() builds a BunxRouteParams object containing req, url, query (deserialized from search params), and
body (parsed JSON for non-GET requests).
4b. Module import
const module = await import(`${file_path}?t=${global.LAST_BUILD_TIME ?? 0}`);
The ?t= cache-buster forces Bun to re-import the module after a rebuild instead of serving a stale cached version.
4c. server() function
If the page exports a server function, it's called with routeParams and its return value becomes serverRes — the props
passed to the component. This is the data-fetching layer (equivalent to Next.js getServerSideProps).
4d. Component instantiation
const Component = module.default as FC;
const component = ;
The default export is treated as the page component, instantiated with the server-fetched props.
▎ If anything above throws (bad route, import error, etc.), the error path falls back to the /500 page (user-defined or
the preset).
---
5. HTML Generation (generate-web-html.tsx)
renderToString(component) is called — importing react-dom/server dynamically from process.cwd()/node_modules/ (the
consumer's React, avoiding the duplicate-instance bug).
The resulting HTML is assembled:
The pageProps (from server()) are serialized via EJSON and injected as window.**BUNEXT_PAGE_PROPS** so the client
hydration script can read them without an extra network request.
---
6. Client Hydration (browser)
The browser:
1. Parses the server-rendered HTML and displays it immediately (no blank flash).
2. Loads /public/pages/index.js (the Bun-bundled client script).
3. That script calls hydrateRoot(container, ) — React attaches event handlers
to the existing DOM rather than re-rendering from scratch.
At this point the page is fully interactive.
---
7. HMR (dev only)
The injected EventSource("/\_\_hmr") maintains a persistent SSE connection. When the watcher detects a file change, it
rebuilds all pages, updates LAST_BUILD_TIME, and sends event: update\ndata: reload down the SSE stream. The browser
calls window.location.reload(), which re-requests the page and repeats steps 4–6 with the fresh module.