Deploy to Vercel
Single Rust binary on Vercel functions, static assets on the CDN, streaming preserved
A nextrs app deploys to Vercel as one Rust binary behind a catch-all rewrite. Vercel's Fluid compute runs the function and supports HTTP response streaming, so the loading→page swap works in production. This is the officially supported way to host an Axum app on Vercel — nextrs just needs one adapter (below) to keep HTML streaming intact.
Project layout
Vercel auto-detects Cargo.toml and builds Rust functions from api/. Three pieces:
1. A [[bin]] entry pointing at api/index.rs:
[[bin]]
name = "index"
path = "api/index.rs"
[dependencies]
nextrs = { version = "0.1", features = ["vercel"] }
vercel_runtime = { version = "2", features = ["axum"] }
2. api/index.rs — the entire function:
use nextrs::vercel::StreamingVercelLayer;
use tower::ServiceBuilder;
use vercel_runtime::Error;
include!(concat!(env!("OUT_DIR"), "/nextrs_routes.rs"));
#[tokio::main]
async fn main() -> Result<(), Error> {
let router = nextrs::router::build_router(generated_registry());
let app = ServiceBuilder::new()
.layer(StreamingVercelLayer::new())
.service(router);
vercel_runtime::run(app).await
}
The registry is the same one your local main.rs consumes — generated by build.rs from the app/ tree, so both entry points always serve identical routes.
3. vercel.json — route everything to the function:
{
"rewrites": [{ "source": "/(.*)", "destination": "/api/index" }]
}
Dynamic segments need no Vercel-side configuration — the catch-all passes the full path through and Axum matches it.
Deploy with vercel deploy.
Streaming through the Vercel adapter
The stock vercel_runtime::axum::VercelLayer only streams responses whose content-type is text/event-stream or application/json. nextrs streams text/html, so the stock layer silently buffers the whole response — TTFB equals total time and the loading shell is pointless.
nextrs::vercel::StreamingVercelLayer (behind the vercel cargo feature) is a drop-in replacement that streams unconditionally. Non-streaming responses are unaffected. If you ever see TTFB ≈ total on a deployed streaming route, check that you're using it.
Static assets on the CDN
Vercel serves files from a root-level public/ directory at root URL paths before applying rewrites, with edge caching. Since your assets live next to your app at site/public/, mirror them at build time from the workspace root's build.rs:
nextrs::build::sync_public_dir("site/public", "public")
.expect("sync_public_dir failed");
The root public/ is a generated artifact — gitignore it. Deployed assets come back with x-vercel-cache: HIT at ~145ms; the function never sees those requests.
What to expect on latency
Measured on a real deploy (warm, p50): non-streaming pages ~220–250ms TTFB; streaming routes show the shell at ~230ms with the page following whenever its data resolves; CDN-cached assets ~145ms. Cold starts add roughly 250–330ms, paid once per warm cycle — Fluid compute keeps Rust functions warm aggressively.
Verify after deploying
curl -o /dev/null -w "TTFB=%{time_starttransfer}s total=%{time_total}s\n" \
https://your-deployment.vercel.app/with-loading
TTFB << total means streaming survived the trip. (Preview URLs behind Vercel SSO need an x-vercel-protection-bypass header.) More verification recipes in Streaming.