Deploy to Vercel
Run the shared Rust app as one Vercel function, regenerate frontend assets during the build, and preserve streaming
A nextrs app deploys to Vercel as one Rust binary behind a catch-all rewrite.
Static files and generated browser bundles are served from public/; dynamic
requests reach the Axum router through a thin Vercel adapter.
The scaffold includes this target by default. It also defaults to prebuilt
deploys with git auto-builds disabled, because local Rust builds avoid the
cloud build queue. The vercel.json build remains self-contained if you
choose to re-enable cloud builds.
One application, two process adapters
Application construction belongs in src/app.rs:
// src/app.rs
include!(concat!(env!("OUT_DIR"), "/nextrs_routes.rs"));
pub fn app() -> axum::Router {
let public = concat!(env!("CARGO_MANIFEST_DIR"), "/public");
nextrs::router::build_router_with_public(generated_registry(), public)
.merge(nextrs::openapi::spec_router(generated_openapi()))
}
src/main.rs starts that app locally. Vercel currently requires its Rust
function at api/index.rs, so the scaffold supplies a second, deliberately
thin process adapter:
// api/index.rs -- do not put application logic here
use nextrs::vercel::StreamingVercelLayer;
use tower::ServiceBuilder;
#[tokio::main]
async fn main() -> Result<(), vercel_runtime::Error> {
let app = ServiceBuilder::new()
.layer(StreamingVercelLayer::new())
.service(my_app::app());
vercel_runtime::run(app).await
}
Both processes call the same app(), so routes and application layers cannot
drift.
The corresponding Cargo targets and runtime dependencies are:
[lib]
path = "src/app.rs"
[[bin]]
name = "my-app"
path = "src/main.rs"
[[bin]]
name = "index"
path = "api/index.rs"
[dependencies]
nextrs = { version = "0.5", features = ["vercel"] }
tower = "0.5"
vercel_runtime = { version = "2", features = ["axum"] }
Vercel configuration
The generated vercel.json installs and builds from the application root:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"installCommand": "npm ci",
"buildCommand": "npm run client:prepare && cargo build --release --bin index && npm run client:build",
"functions": {
"api/index.rs": { "runtime": "vercel-rust@4.0.11" }
},
"headers": [
{
"source": "/dist/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
],
"rewrites": [
{ "source": "/(.*)", "destination": "/api/index" }
],
"git": { "deploymentEnabled": false }
}
The functions block is mandatory because vercel-rust is a community
runtime and is not selected automatically. The catch-all passes the original
path to Axum, including dynamic segments.
The build sequence is intentional:
npm ciinstalls root dependencies and links.nextrs/client.client:preparedumps.nextrs/openapi.jsonand generates both client surfaces.- the release Cargo build discovers routes and bundles React pages into
public/distwhile compiling the Vercel function; client:buildemits the package's JavaScript and.d.tsfiles.
Do not set NEXTRS_SKIP_BUNDLE for a normal deploy. public/dist,
.nextrs/openapi.json, and the entire .nextrs/client package are disposable
build output. The build materializes the ignored workspace package from its
tracked framework template, then verifies its JavaScript and declarations.
Dependencies are installed once at the app root, never inside the generated
client.
rust-toolchain.toml pins the toolchain used by the TSX bundler dependency
tree. The current scaffold uses Rust 1.96:
[toolchain]
channel = "1.96.0"
If the deploy root has .cargo/config.toml, keep an explicit [build] table
alongside the cargo dev alias. Some vercel-rust versions read
config.build.target during setup:
[alias]
dev = "nextrs dev --bin my-app"
[build]
Deploy
The default scaffold disables git-triggered builds. Use its prebuilt script:
scripts/deploy-prebuilt.sh # production
scripts/deploy-prebuilt.sh --preview # preview
This runs vercel build on your machine and uploads its Build Output. See
Build Locally, Ship Artifacts for setup.
If you prefer Vercel cloud builds, delete the git.deploymentEnabled: false
setting (or enable it in project settings) and push normally. The same root
install/build commands regenerate everything in the cloud; no prebuilt bundle
needs to be checked into git.
Streaming through the adapter
The stock vercel_runtime::axum::VercelLayer only streams a limited set of
content types. nextrs streams text/html, so the stock layer can buffer the
loading shell until the full page is ready.
nextrs::vercel::StreamingVercelLayer streams the Axum response body without
that content-type restriction. Non-streaming responses continue to work. If a
deployed loading route has TTFB approximately equal to total time, first
confirm that api/index.rs installs this layer.
Background work after the response
Detached tokio::spawn work is unsafe in a serverless invocation because the
instance can freeze after sending the response. Use nextrs::WaitUntil:
use nextrs::WaitUntil;
pub async fn post(wait: WaitUntil, Json(req): Json<AddTodoRequest>) -> Json<Todo> {
let todo = add(req.title).await;
let audit = todo.clone();
wait.wait_until(async move {
audit_log(&audit).await;
});
Json(todo)
}
Behind StreamingVercelLayer, the future is registered with the runtime's
shutdown drain. Local and container execution fall back to spawning it. Log
failures inside the future because its output is discarded.
Static assets
Vercel serves root-level public/ files before applying the catch-all rewrite.
That includes user assets such as /logo.svg and the content-addressed files
under /dist/. The generated immutable cache header is safe for /dist/
because changing content produces a different filename.
Verify after deploying
curl -o /dev/null \
-w "TTFB=%{time_starttransfer}s total=%{time_total}s\n" \
https://your-deployment.vercel.app/slow
For a route with a delayed server prefetch, TTFB should be meaningfully less
than total time. Preview URLs protected by Vercel authentication require the
corresponding protection-bypass header.
Removing Vercel support
If Vercel is not a target, remove the whole adapter surface together:
api/index.rs;- the
indexCargo target; vercel_runtime,towerif otherwise unused, and the nextrsvercelfeature if otherwise unused;vercel.jsonand the prebuilt deployment script.
Keep src/app.rs, src/main.rs, and build.rs: they are the shared
application, local process, and Rust build infrastructure, not Vercel code.