Build & Deploy
Build & deploy
x produces optimized production builds: prerendered HTML, island bundles, and a server entry point, all in one command.
Build command
Run x build to produce a production build. The build output goes to a .x/ directory (override with --outDir).
x build[x] resolving routes...[x] found 12 routes[x] building static pages...[x] building island bundles...[x] building server bundle...[x] build complete in 1.2s -> .xx build --outDir dist # write to ./dist instead of .x/
Output structure
The .x/ directory contains everything needed to deploy:
.x/client/ // Static site: prerendered HTML + public/ assetsindex.htmlabout/index.htmlblog/hello-world/index.html_islands/index-abc123/ // One hydration bundle per page's islandsindex-abc123.jsstyles.css // Compiled Tailwindfavicon.icoserver/index.ts // Server entry for SSR/API/actions (run with x start)
Static pages (and their public/ assets) live under .x/client/ and deploy to any static host. Server code ships as .x/server/index.ts, a single entry that re-imports your x.config.ts at runtime.
Static page export
Pages with mode = "static" are rendered at build time, including their loader output. Dynamic segments generate one HTML file per unique path at build time.
Incremental static regeneration
A static page can opt into time-based revalidation with revalidate. The page is prerendered on first request, cached in memory for N seconds, then re-rendered on demand. Each response carries an X-Revalidated header ( hit / miss / none).
export const mode = "static";export const revalidate = 3600; // re-render at most once per hourBust the cache programmatically with a POST to /__x/revalidate — send { "path": "/pricing" } to revalidate one page or an empty body to clear the whole cache.
Production server
Use x start to run .x/server/index.ts. It serves static files and handles dynamic routes, and the entry handles SIGTERM/SIGINT gracefully: it stops accepting connections, flushes the error reporter, and drains in-flight requests for up to 3 seconds before exiting.
x start[x] production server running at http://localhost:3000PORT=8080 x start # PORT env var overrides the default 3000
If a build has no server entry (every page is static and there are no API routes), x start falls back to serving .x/client/ as a plain static file server with an SPA index.html fallback.
Programmatic build
build() from @thexjs/core runs the same pipeline the CLI uses, useful for custom deploy scripts:
import { build } from "@thexjs/core";await build({ pagesDir: "src/pages", apiDir: "src/api", actionsDir: "src/actions", outDir: "dist", configPath: "x.config.ts",});Docker deployment
Deploy with a minimal Docker image using the official Bun runtime:
FROM oven/bun:1 AS buildWORKDIR /appCOPY package.json bun.lock .RUN bun installCOPY . .RUN bun run build:packagesRUN x build --outDir distFROM oven/bun:1-slimWORKDIR /appCOPY --from=build /app/dist distCOPY --from=build /app/node_modules node_modulesEXPOSE 3000CMD ["x", "start", "--outDir", "dist"]Deploy to Vercel
Vercel doesn't run a long-lived Bun process, so it uses a different adapter: @thexjs/adapter-vercel builds a .vercel/output/ tree (Build Output API v3) directly, so no vercel.json is required.
bun add -d @thexjs/adapter-vercelx build --adapter vercelvercel deploy --prebuilt
If every page in your app uses mode = "static" (no API routes, no server actions), no function is emitted at all, just static files and filesystem routing. Server-mode pages, API routes, and server actions run inside a single bundled nodejs20.x function.