Deploying
Deploy with text build --adapter static, bun, vercel or cloudflare, or run text start behind a proxy.
text build produces a portable dist/. An adapter turns it into the output a platform expects.
Adapters
text build --adapter <name>| Adapter | Output | Runs |
|---|---|---|
| (none) | dist/ + dist/.text/server | text start (Node) |
static | dist/ | Any static host |
bun | dist/server.js | bun dist/server.js |
vercel | .vercel/output | Vercel (Build Output API v3) |
cloudflare | dist/ + _worker.js | Cloudflare Pages |
The adapter can also come from TEXTJS_ADAPTER. When text build runs on Vercel (VERCEL=1) or Cloudflare Pages (CF_PAGES=1), the matching adapter is selected automatically.
Every adapter runs the same pipeline as text start: middleware, rewrites, route handlers with the route cache, static assets, then the console document for page URLs. The server adapters serve hashed files under /assets/ with Cache-Control: public, max-age=31536000, immutable.
Static
text build --adapter staticFor hosts without a server (GitHub Pages, S3, any CDN). GET route handlers with dynamic = "force-static" or revalidate are prerendered into files: app/api/feed.json/route.ts becomes dist/api/feed.json. A copy of index.html is written to 404.html so deep links open the console app.
The build fails, listing every reason, if the app needs a server: middleware.ts, server-rendered routes, dynamic route handlers, dynamic segments or non-GET methods. Configure rewrites on the host instead.
Bun
text build --adapter bunPORT=8080 bun dist/server.jsA single self-contained server with no node_modules needed at runtime. It reads PORT (default 3000) and HOSTNAME (default 0.0.0.0), and never serves server.js or .text/. It is a long-lived process, so it also hosts server-rendered routes (export const runtime = "server") and their live sessions, like text start. Use it in a container:
FROM oven/bun:1COPY dist /appCMD ["bun", "/app/server.js"]Vercel
text build --adapter vercelvercel deploy --prebuiltOr push to a Git repository connected to Vercel: the build runs text build, detects Vercel and writes .vercel/output.
| Output | Contents |
|---|---|
static/ | The client build (under basePath) |
functions/_textjs/server.func | One Node.js 22 function serving every route handler, with streaming |
functions/_middleware.func | middleware.ts as Edge Routing Middleware |
config.json | Asset headers, middleware matchers, rewrites, handler routes and the console document |
The edge middleware speaks the x-middleware-next / x-middleware-rewrite protocol, so header overrides and rewrites happen in Vercel's router before the function or CDN is reached. Matchers become route src patterns (with has / missing), so unmatched requests never invoke the middleware. The handler function is internal: requests for /_textjs/* return 404, and requests carrying the internal __textjs_path parameter return 400, so it is only reachable through the routes the adapter writes.
Cloudflare Pages
text build --adapter cloudflarebunx wrangler pages dev dist --compatibility-flags=nodejs_compatbunx wrangler pages deploy distThe output uses Pages advanced mode:
| File | Purpose |
|---|---|
_worker.js | Middleware, rewrites, route handlers and the console document |
_routes.json | Keeps /assets/* on Pages' static CDN (unless your matcher covers it, or the app has a basePath) |
_headers | Immutable caching for /assets/* (appended to yours) |
404.html | Makes Pages answer missing files with 404 instead of the app (kept if you have one) |
.text/ (server bundles and build manifests) is removed from dist/, since Pages publishes the directory as-is.
Enable the nodejs_compat compatibility flag in the Pages project (Settings → Runtime) or in wrangler.toml; the data cache uses AsyncLocalStorage. Static files are served through the ASSETS binding, and background revalidation uses ctx.waitUntil().
name = "my-console"pages_build_output_dir = "dist"compatibility_date = "2025-01-01"compatibility_flags = ["nodejs_compat"]Server-rendered routes
Server-rendered routes keep a session per open tab, so they need a long-lived server: text start or the bun adapter. The static, vercel and cloudflare adapters fail the build with TX030 when a page exports runtime = "server".
Behind a proxy
text start binds to 127.0.0.1 and, while bound to loopback, only answers localhost, *.localhost and IP-address hosts (DNS-rebinding protection). Put the public settings in text.config.ts:
import { defineConfig } from "@textjs/next";export default defineConfig({ server: { origin: "https://console.example.com", // request.url always uses it (or TEXTJS_ORIGIN) trustProxy: true, // read X-Forwarded-Host / -Proto from your proxy (or TRUST_PROXY=1) allowedHosts: [".example.com"], },});Without origin or trustProxy, request.url is built from the validated Host header over http.
Caveats
proxyintext.config.tsonly runs intext devandtext start; adapters don't include it. Userewrites, which every server adapter supports.- Serverless instances each have their own in-memory cache. See Caching.
rewritesthat use raw regular-expression groups translate to Vercel routes on a best-effort basis; named parameters (:slug,:path*) are exact.