Deployment
Build deployable output with Farm's deploy config and Nitro presets. Farm owns route discovery, framework conventions, and app bundling; Nitro owns the final server output shape.
Scheduled routes are configured through framework Cron. Vercel builds receive native Build Output API cron entries, Cloudflare Worker builds receive Wrangler triggers, and other adapters can consume .farm/cron-manifest.json.
Dynamic pages and APIs can select their execution runtime, regions, and duration with file exports, layout defaults, programmatic routes, or route rules. See Route Runtime for precedence, provider support, and generated deployment output.
Target-based deploy config
export default defineConfig({
deploy: {
target: "vercel",
output: ".vercel/output",
},
});First-class targets
| Target | Preset | Default output |
|---|---|---|
| vercel | vercel | .vercel/output |
| cloudflare | cloudflare-pages | .farm/.output |
| netlify | netlify | dist + .netlify/functions-internal |
| node | node-server | .farm/.output |
These targets get the most polished Farm defaults. They map deploy.target to a Nitro preset, output directory, and the matching farm deploy command when Farm has a deploy wrapper for that platform.
When no target or preset is configured, Farm recognizes platform CI through VERCEL, NETLIFY,
and CF_PAGES. Detection is enabled only by the values 1 or true (case-insensitive); unset,
empty, 0, false, and other values do not select a platform. An explicit target, preset, or CLI
override remains authoritative.
Immutable fingerprinted assets on Vercel
Farm's Vercel Build Output automatically serves content-hashed files under assets/ and chunks/
with Cache-Control: public, max-age=31536000, immutable. This covers fingerprinted JavaScript, CSS,
fonts, and images, including nested asset directories and applications with a basePath.
The rule intentionally excludes HTML, unhashed public files, and stable entry URLs such as
/farm-client.js and /farm-client.css. Those URLs can change between deployments and must remain
revalidatable. The immutable header route runs before Vercel's filesystem handler, so matching files
are still served directly from the CDN and never enter the Farm server function.
Preset showcase
Use deploy.target when you want one config file to control the platform output. Farm maps that target to the Nitro preset, default output directory, and deploy command shape.
| Platform | Config | Build command | Deploy command | Output to inspect |
|---|---|---|---|---|
| Vercel | target: "vercel" | FARM_DEPLOY_TARGET=vercel farm build | farm deploy --vercel --prod | .vercel/output |
| Cloudflare Pages | target: "cloudflare" | FARM_DEPLOY_TARGET=cloudflare farm build | farm deploy --cloudflare | .farm/.output |
| Netlify | target: "netlify" | FARM_DEPLOY_TARGET=netlify farm build | farm deploy --netlify | dist + .netlify/functions-internal |
| Self-hosted Node | target: "node" | FARM_DEPLOY_TARGET=node farm build | node .farm/.output/server/index.mjs | .farm/.output |
import { defineConfig } from "@farm.js/core";
const target = process.env.FARM_DEPLOY_TARGET ?? "vercel";
export default defineConfig({
deploy: {
target,
cloudflare: {
projectName: process.env.CLOUDFLARE_PAGES_PROJECT,
},
netlify: {
site: process.env.NETLIFY_SITE_ID,
},
},
});{
"scripts": {
"build": "farm build",
"build:vercel": "FARM_DEPLOY_TARGET=vercel farm build",
"build:cloudflare": "FARM_DEPLOY_TARGET=cloudflare farm build",
"build:netlify": "FARM_DEPLOY_TARGET=netlify farm build",
"build:self-host": "FARM_DEPLOY_TARGET=node farm build",
"start:self-host": "node .farm/.output/server/index.mjs",
"deploy:vercel": "farm deploy --vercel --prod",
"deploy:cloudflare": "farm deploy --cloudflare",
"deploy:netlify": "farm deploy --netlify"
}
}The same shape lives in examples/deployment-presets so the preset behavior can be tested from a real app.
Chunk error recovery
Farm installs client-side chunk recovery automatically in the generated browser runtime. When the browser fails to load a JavaScript or CSS chunk after a deployment, Farm reloads the current page once so the app can pick up the newest asset manifest.
This is meant for stale deploy assets, where a user has an older HTML page open while the server now points at newer chunks. Farm stores a short session guard per page before reloading, so repeated chunk failures do not trap the user in a reload loop. Ordinary runtime errors are left alone and should still be handled with route error.tsx, programmatic error components, monitoring, and tests.
Farm's browser error overlay and its source-map diagnostics are development-only and are excluded from production client bundles. Production client-plugin error hooks and chunk recovery remain available; no configuration change is required.
Rolling deployment safety
Farm embeds a deployment ID in each HTML document and sends it with SPA data, RSC navigation, and server action requests. The server also stores the ID in an HttpOnly, SameSite=Lax cookie so a progressively enhanced form action carries the same protection before JavaScript loads.
During a rolling deployment, an open page can belong to release A while its next request reaches release B. Farm handles that mismatch according to whether replay is safe:
| Request | Mismatch behavior |
|---|---|
| SPA page data, RSC navigation, or HTML fetch | Emit farm:deployment-mismatch and perform a full document navigation. |
| JavaScript server action | Return 409 FARM_DEPLOYMENT_MISMATCH; never retry the mutation automatically. |
| Form server action without JavaScript | Return the same non-cacheable 409 response using the deployment cookie. |
Mutation requests are rejected before their action body is decoded or the handler runs. This prevents a stale action from being replayed against a new release and accidentally creating duplicate writes. The user can refresh and deliberately submit again.
Farm resolves the ID from explicit config, FARM_DEPLOYMENT_ID, VERCEL_GIT_COMMIT_SHA, or CF_PAGES_COMMIT_SHA. Otherwise, production uses generateBuildId and development uses "development".
import { defineConfig } from "@farm.js/core";
export default defineConfig({
deploymentId: process.env.RELEASE_ID,
});Use a commit SHA or CI release ID shared by every server instance in one deployment. A random value created independently when each process starts will make healthy instances reject one another's requests.
Applications can observe recovery and report it without replacing Farm's default behavior:
window.addEventListener("farm:deployment-mismatch", (event) => {
const error = (event as CustomEvent).detail;
reportError(error.code, {
clientDeploymentId: error.clientDeploymentId,
serverDeploymentId: error.serverDeploymentId,
});
});The deployment ID is a consistency marker, not a secret or an authorization boundary. Continue to authenticate and authorize every server action.
Self-host a Farm app
Use target: "node" when you want to run the app on your own server, VPS, container, or platform that expects a long-running Node process. Farm builds the app with Nitro's Node server preset.
import { defineConfig } from "@farm.js/core";
export default defineConfig({
deploy: {
target: "node",
output: ".output",
},
});{
"scripts": {
"build": "farm build",
"start": "node .output/server/index.mjs"
}
}Build once, then run the generated server:
pnpm build
HOST=0.0.0.0 PORT=3000 pnpm startfarm start is equivalent for the node target and adds --port/--host flags plus clear errors when the configured target has no local server. It runs through the farm CLI, which is a devDependency, so on hosts that remove devDependencies before starting the app, use the node command instead. New starters use it as their start script.
For a VPS, run the same start command behind nginx, Caddy, systemd, or a process manager such as PM2. Environment variables should be provided by the host at runtime, not committed into the bundle.
Docker
farm build writes a self-contained server, so a multi-stage image installs everything to build and copies only the output into the runtime stage:
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . ./
RUN npm run build
FROM node:22-slim
WORKDIR /app
COPY --from=build /app/.farm/.output ./.farm/.output
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", ".farm/.output/server/index.mjs"]The build stage keeps devDependencies because farm build runs the CLI. The runtime stage does not need node_modules. This file uses the default output directory, .farm/.output; if you set deploy.output, copy and start that directory instead. Add a .dockerignore with node_modules, .farm, and .git so local builds stay out of the image.
Hosting platforms
Any platform that builds from a Git repository and runs a long-lived Node.js process can host a Farm app, including Coolify, Dokploy, Railway, Render, Fly.io, Zeabur, Clever Cloud, and Heroku. They need the same settings:
| Setting | Value |
|---|---|
| Node.js | 22.13 or later |
| Build command | npm run build (runs farm build) |
| Start command | npm start (runs node .farm/.output/server/index.mjs in new starters) |
| Port | The server reads PORT (or NITRO_PORT) from the platform, else 3000 |
Buildpack builders such as Nixpacks and Railpack detect a Node.js app and run these scripts without extra configuration. Platforms that build from a container image can use the Dockerfile above.
A few details decide whether the first deploy works:
- Install devDependencies for the build.
farm buildneeds@farm.js/cli. If the platform installs production dependencies only, enable devDependencies for the build step or deploy with the Dockerfile. - Start with
node, notfarm start, when devDependencies are pruned. Some builders remove devDependencies before the app starts, andfarm startcomes from the CLI. - Build for Node.js. If
farm.config.tssetsdeploy.targetto another platform, such asvercel, remove it or set it tonode. A platform target builds output that only that platform can serve.
Nitro preset pass-through
Farm can also build for any Nitro preset that Nitro can resolve. Use deploy.preset when the target is not one of Farm's first-class shortcuts, or pass --preset from the CLI for a one-off build.
import { defineConfig } from "@farm.js/core";
export default defineConfig({
deploy: {
preset: process.env.NITRO_PRESET || "node-server",
output: ".output",
},
});NITRO_PRESET=aws-lambda farm build
# Or, in an app that does not also set deploy.target:
farm build --preset deno-deploy
farm build --preset my-company-presetFor built-in Nitro presets, the generated output follows Nitro's provider shape. For custom presets, Farm passes the preset name through to Nitro, so the preset must be installed or otherwise resolvable by Nitro in the project.
Nitro version compatibility
Farm pins the Nitro and H3 versions used by its production adapters. Do not override either package independently: Nitro beta releases can change builder exports, runtime entry points, event shapes, and provider output layouts together.
The current Farm dependency uses a patched Nitro v3 beta newer than 3.0.260429-beta, the first v3 release containing the wildcard-redirect and encoded proxy-traversal fixes. Applications upgrading from a Farm release that used the Nitro v3 alpha should remove any app-level Nitro override or alpha compatibility patch, reinstall dependencies to refresh the lockfile, and rerun type-checking plus a production build for every deployment target. Applications without those overrides need no configuration migration.
Nitro coverage
Nitro's official deploy docs list these runtime and provider families. Farm's first-class deploy commands cover the common Vercel, Cloudflare Pages, and Netlify path, while deploy.preset / farm build --preset lets advanced apps target the rest.
| Family | Nitro supports |
|---|---|
| Runtimes | Node.js, Bun, Deno |
| Zero-config providers in Nitro CI/CD | AWS Amplify, Azure, Cloudflare, Firebase App Hosting, Netlify, StormKit, Vercel, Zeabur |
| Other Nitro provider docs | Alwaysdata, AWS Lambda, Cleavr, Deno Deploy, DigitalOcean, EdgeOne Pages, Firebase, Flightcontrol, Genezio, GitHub Pages, GitLab Pages, Heroku, IIS, Koyeb, Platform.sh, Render.com, Zephyr Cloud, Zerops |
Common preset names include node-server, bun, deno-server, deno-deploy, aws-lambda, aws-amplify, azure-swa, cloudflare-pages, cloudflare-module, firebase-app-hosting, github-pages, gitlab-pages, netlify, netlify-edge, vercel, zeabur, and zerops. Nitro can add or rename presets over time, so check Nitro's deploy docs when targeting a provider directly.
Build
pnpm build
FARM_DEPLOY_TARGET=vercel farm build
farm build --preset aws-lambda
farm deploy --cloudflareCompact config
Farm reads deployment settings from farm.config.ts, so a minimal project does not need vercel.json, wrangler.toml, or Netlify config just to choose an output target.
import { defineConfig } from "@farm.js/core";
export default defineConfig({
deploy: {
target: "vercel",
},
});When target is vercel, Farm uses the Vercel Nitro preset and writes Build Output API output to .vercel/output. Netlify writes the server function to .netlify/functions-internal. Other targets default to .farm/.output unless you override output or outputDir. When preset is set directly, Farm passes that value through to Nitro.
farm deploy --vercel requires .vercel/output because the Vercel CLI reads that fixed directory
for --prebuilt uploads. Keep the default output for Farm-managed Vercel deploys. A custom Vercel
output directory is only suitable when another deployment workflow uploads the build itself.
For Netlify, Nitro writes static assets to dist and the server function to
.netlify/functions-internal, the directory Netlify reads functions from without configuration. A
Git-connected Netlify site needs only farm build as the build command and dist as the publish
directory. farm deploy --netlify uploads the same output with --no-build, so the provider deploys
the exact artifact Farm just verified instead of starting a second build. With a custom Netlify
output, set [build] functions to <output>/server in netlify.toml.
farm deploy --netlify passes that path for you.
Override output
Use output for the compact form or outputDir when you want the explicit option name.
export default defineConfig({
deploy: {
target: "netlify",
output: ".output",
},
});Both options belong inside deploy. A top-level output: "standalone", "static", or "export"
does not select Farm rendering behavior. Choose a deployment target here, then configure static
pages with dynamic = "force-static" or routeRules as described in
Server Rendering.
Platform deploys
| Command | What it expects |
|---|---|
FARM_DEPLOY_TARGET=vercel farm build | Builds .vercel/output for Vercel prebuilt deploys. |
FARM_DEPLOY_TARGET=node farm build | Builds .farm/.output/server/index.mjs for self-hosted Node. |
farm build --preset <nitro-preset> | Builds with any Nitro preset that Nitro can resolve. |
farm deploy --vercel | Uses vercel deploy --prebuilt. |
farm deploy --cloudflare | Deploys the Cloudflare Pages output with Wrangler. |
farm deploy --netlify | Deploys the Netlify output with Netlify CLI. |
farm deploy --plan | Prints the resolved build and deploy operations only. |
For other Nitro presets, use the host's documented deploy command or CI workflow after farm build writes the Nitro output.
Use farm deploy --plan before a release when you want to verify the selected target, preset, runtime, output directory, and exact build and provider commands. Planning is read-only: it does not run the build, require a provider login, inspect existing output, or deploy anything.
Environment variables
Keep environment variables in the platform's environment manager or local .env files. Farm config can reference process.env, and integrations should validate required provider keys during setup.
Server values declared under env.server are validated when the app builds, but they are not
stored in the build output. The production server reads them from its own environment when code
calls getEnv or serverEnv, so set them wherever the server runs, and a restart picks up a
rotated secret. On Cloudflare Workers, Farm also reads the worker's bindings and secrets. A value
read at runtime keeps the type its parser produced at build time, such as a number or boolean. When a
parser does more than convert the raw string to a string, number, boolean, or JSON value, such as
splitting a list or scaling a number, Farm cannot repeat it at runtime, so that parsed value stays in
the build output. Values declared under env.public are part of the client bundle, so never put a
secret there.
export default defineConfig({
integrations: {
billing: stripe({
secretKey: process.env.STRIPE_SECRET_KEY,
}),
},
});Preview deployments
Set agent.noindexPreviews to keep preview deployments out of search results:
export default defineConfig({
agent: { noindexPreviews: true },
});On a preview, every response from Farm's server (pages, API routes, docs, errors) gets
X-Robots-Tag: noindex, nofollow. Production deployments are unaffected. The option lives with the
other agent readiness settings.
A deployment is a preview when:
| Signal | Where |
|---|---|
FARM_PREVIEW=1 (0 turns it off) | Any host, farm dev, farm build, or the server |
VERCEL_ENV=preview | Vercel previews and custom environments |
CONTEXT=deploy-preview or branch-deploy | Netlify, with NETLIFY set, read at build time |
IS_PULL_REQUEST=true | Render pull request previews |
Farm reads these when it builds and again when the server starts, so a preview build stays one at
runtime (Netlify sets CONTEXT only during the build); an explicit FARM_PREVIEW at runtime wins
over the build. Cloudflare Pages exposes no variable that tells a preview from production, so set
FARM_PREVIEW=1 in its preview environment; Cloudflare already sends X-Robots-Tag: noindex on
*.pages.dev preview URLs. Run FARM_PREVIEW=1 farm dev to check the behavior locally.
Files a platform serves straight from its CDN or static file server, such as prerendered pages and assets, do not pass through Farm's server and keep their platform's headers. Vercel, Netlify, and Cloudflare Pages already mark their generated preview URLs as noindex; a preview on a custom domain only gets the header on responses from Farm's server.
Production checklist
- Run
farm buildbeforefarm deploy. - For self-hosting, run the generated server (
node .farm/.output/server/index.mjswith the default output directory) behind your process manager or container runtime. - Confirm the selected target or preset matches
deploy.target,deploy.preset, or the CLI/env override. - For non-first-class platforms, confirm the Nitro preset name and provider deploy command from Nitro's docs.
- Check generated output exists at the resolved output directory.
- Run provider-specific login commands such as
vercel login,wrangler login, ornetlify loginbefore deploying. - Keep provider secrets out of client bundles and UI registry components.
Build deployable output with Farm's deploy config and Nitro presets, from first-class targets to custom Nitro output.