Configuration
Use farm.config.ts as the single project control plane for source paths, integrations, docs, KV storage, database clients, deployment, and framework behavior.
Define config
import { defineConfig } from "@farm.js/core";
export default defineConfig({
deploy: {
target: "vercel",
},
docs: {
entry: "/docs",
},
md: {
expose: ["/", "/pricing"],
cache: 60,
},
mdx: {
components: "./src/markdown-components.tsx",
},
theme: {
default: "system",
},
});srcDir defaults to "src". Set it only when the application source lives somewhere else.
defineConfig is the canonical Farm helper. defineFarmConfig remains available as a deprecated exact alias for existing applications.
Programmatic development servers
Without a config file, createServer and startDevServer from @farm.js/core/server
resolve the supplied config, including plugins, integrations, layers, environment/API
resolvers, redirects, headers, and rewrites:
import { defineConfig } from "@farm.js/core";
import { startDevServer } from "@farm.js/core/server";
const server = await startDevServer(
defineConfig({
root: process.cwd(),
redirects: () => [{ source: "/old", destination: "/" }],
}),
3000,
);
// When the owning process is finished with the server:
await server.close();If Farm finds a config file under root, that file remains authoritative; the inline
object is not merged over it. An explicit port passed to startDevServer(config, port)
or createServer(config, { port }) still wins. createServer returns an unlistened
Vite server; startDevServer also listens and starts Farm's runtime lifecycle.
You can pass an already-resolved config without rerunning its environment parsers or
generating its integration/layer plugins again. Runtime-form config fields remain
supported. Config callbacks resolve during creation, not on each Vite restart. The
same plugin runtime survives a restart; final server.close() owns cleanup.
TypeScript
Farm transpiles TypeScript through the selected renderer and Vite. It reads the project's normal
tsconfig.json, but farm build does not run a separate project type check. Keep type checking as
an explicit script so the same command runs locally and in CI:
{
"scripts": {
"type-check": "tsc --noEmit"
}
}A top-level typescript.tsconfigPath or typescript.ignoreBuildErrors setting has no effect in
Farm. Configure compiler behavior in tsconfig.json; point an explicit tsc -p command at a
different file when needed.
Renderer
React remains the default renderer, so existing applications and configurations do not need to change. Select another renderer when you want to author the UI with that library while keeping FARMJS routing and server features. See Renderers for the feature matrix and dedicated React, Preact, Solid, Vue, and Svelte guides.
Preact
Install Preact and its FARMJS renderer adapter:
pnpm add @farm.js/preact preactimport { defineConfig } from "@farm.js/core";
import { preact } from "@farm.js/preact";
export default defineConfig({
renderer: preact(),
});Preact routes use .tsx or .jsx. The adapter configures Preact JSX, Prefresh, React compatibility
aliases, server rendering and streaming, and browser hydration. See the
Preact Renderer guide for typed server calls and compatibility boundaries.
Create a ready-to-run Preact application from the CLI:
pnpm create @farm.js/app my-preact-app --template basic --renderer preact --typescriptSvelte
Install the Svelte adapter and runtime:
pnpm add @farm.js/svelte svelteimport { defineConfig } from "@farm.js/core";
import { svelte } from "@farm.js/svelte";
export default defineConfig({
renderer: svelte(),
});Routes can then use Svelte 5 components directly:
src/app/layout.svelte
src/app/page.svelte
src/app/products/[id]/page.svelteSee Svelte Renderer for module route exports, layout snippets, hydration, typed server calls, and current compatibility boundaries.
Create a ready-to-run Svelte application from the CLI:
pnpm create @farm.js/app my-svelte-app --template basic --renderer svelte --typescriptVue
Install Vue and its FARMJS renderer adapter:
pnpm add @farm.js/vue vueimport { defineConfig } from "@farm.js/core";
import { vue } from "@farm.js/vue";
export default defineConfig({
renderer: vue(),
});Routes can then use Vue Single-File Components directly:
src/app/layout.vue
src/app/page.vue
src/app/products/[id]/page.vueFARMJS compiles the SFCs with Vue's Vite plugin, renders them with createSSRApp and
renderToString, and hydrates interactive routes in the browser. Layout children are exposed through
Vue's default <slot />. See the Vue server-rendering guide
for Vue-specific SSR constraints.
See Vue Renderer for SFC route exports, hydration, typed server calls, and current compatibility boundaries.
Create a ready-to-run Vue application from the CLI:
pnpm create @farm.js/app my-vue-app --template basic --renderer vue --typescriptSolid
Install the Solid adapter and runtime:
pnpm add @farm.js/solid solid-jsimport { defineConfig } from "@farm.js/core";
import { solid } from "@farm.js/solid";
export default defineConfig({
renderer: solid(),
});The renderer controls component compilation, server rendering, and browser hydration. FARMJS continues to own routing, layouts, API routes, middleware, data access, observability, and deployment, so those server features use the same APIs with every renderer. UI code uses the selected library's native primitives—for example, Solid signals instead of React hooks.
See Solid Renderer for route conventions, client boundaries, typed server calls, and current compatibility boundaries.
Create a ready-to-run Solid application directly from the CLI:
pnpm create @farm.js/app my-solid-app --template basic --renderer solid --typescriptOmitting renderer selects React. The renderer option is currently available for the Basic starter;
integration starters continue to use React while their UI packages are migrated individually.
Docs config
Configure the docs runtime directly in farm.config.ts:
import { defineConfig } from "@farm.js/core";
export default defineConfig({
docs: {
entry: "/docs",
metadata: {
description: "Product guides and API reference.",
},
nav: {
title: "Acme Docs",
},
search: {
provider: "simple",
enabled: true,
},
pageActions: {
copyMarkdown: {
enabled: true,
},
},
llmsTxt: true,
sitemap: true,
robots: true,
},
});This single property enables human-readable pages, markdown mirrors, search metadata, and
agent-readable docs routes. A separate docs.config.* or docs.json file is optional and intended
only for large serializable configurations; inline values always take priority. See
Docs Engine for content layout, generated routes, and API overrides.
The same search option configures the search provider and the docs interface. When it is enabled,
Farm mounts the shared Omni React search from @farming-labs/theme. The sidebar control and
Cmd+K on macOS or Ctrl+K elsewhere open the same search interface used by the other Farming Labs
framework adapters. Set search: false or search.enabled: false to remove the control, client
mount, and shortcut together.
Important options
| Option | Use it for |
|---|---|
| extends | Composing local or package Farm layers with project-first overrides. |
| srcDir | Changing the app source folder from the default src. |
| renderer | Selecting React (default) or an adapter such as Preact, Svelte, Vue, or Solid. |
| api | Configuring the public root used by Farm's typed browser API client. |
| integrations | Registering built-in or custom integrations. |
| auth | Enabling Farm's built-in email/password auth, sessions, helpers, and hooks. |
| mcp | Composing API routes and standalone tools in one authenticated MCP server. |
| agent | Agent readiness: llms.txt and JSON-LD by default, crawler rules, and an API catalog. |
| theme | Enabling light, dark, and system modes with client and server APIs. |
| storage | Configuring KV drivers/mounts and, in the current beta, an integration DB client. |
| migrations | Running one-shot schema/provider commands with farm migrate. |
| schema | Allowing plugins to add columns (allowExtend) and foreign keys (allowForeignKeys). |
| cron | Mapping portable UTC schedules to ordinary GET API routes. |
| i18n | Configuring locale routes, detection, message catalogs, typing, and direction. |
| docs | Serving the built-in docs runtime and docs API. |
| md | Restricting or disabling automatic markdown mirrors like /pricing.md. |
| mdx | Rendering page.md and page.mdx app routes, plus MDX components. |
| telemetry | Controlling automatic production-site reporting to Farm's usage dashboard. |
| analytics | Turning off request and MCP analytics for an app linked to a Farm Infra project. |
| deploy | Selecting a target, preset, and output directory. |
| deploymentId | Detecting stale browser requests during rolling deployments. |
| trailingSlash | Choosing the canonical URL shape for application page routes and links. |
| routeRules | Applying rendering, cache, redirect, CORS, and header behavior to route patterns. |
| security | Applying an app-wide CSP with an enforcing or report-only response header. |
| serverActions | Restricting trusted action origins and request body size. |
| images | Configuring responsive widths, remote allowlists, formats, and optimizer limits. |
| performance | Budgeting image and font preload hints without changing the rendered resources. |
| experimental | Auditing or enabling opt-in rendering experiments such as isolated hydration and PPR. |
| openapi | Publishing API reference docs. |
| vite | Passing extra Vite options such as plugins, define, resolve.alias, and css. |
Vite config
The vite field applies to farm dev and to both production builds in farm build (client and
server). Plugins, define, resolve, css, ssr, and other Vite options merge with Farm's own
config the same way in each.
export default defineConfig({
vite: {
define: { __APP_VERSION__: JSON.stringify(process.env.APP_VERSION) },
css: { modules: { localsConvention: "camelCase" } },
},
});Farm keeps the options it needs to produce its production output: root, base, mode,
appType, publicDir, the build's entries and output (build.outDir, build.emptyOutDir,
build.ssr, build.ssrEmitAssets, build.write, build.lib, build.watch,
build.rollupOptions.input, and build.rollupOptions.output), and any build, define, or
esbuild value Farm sets for that build. Use basePath and publicDir in Farm's config instead.
During development, Farm loads server modules through Vite's ssr environment runner. With
React Server Components enabled, RSC entries use a separate rsc runner and resolution conditions;
the browser uses client. Vite owns runner caching, hot updates, and cleanup when the server closes.
Custom server environments must expose an in-process module runner. A fetch-only remote runtime
needs a request/response adapter; selecting the Cloudflare deployment target does not yet run
farm dev inside workerd.
Application base path
Set basePath when the complete application is mounted below the origin root. Farm applies the
canonical path consistently to routes, links, assets, and runtime endpoints:
export default defineConfig({
basePath: "/console",
});Farm accepts a leading-slash or bare pathname and removes duplicate and trailing slashes. It rejects
URLs, query strings, hashes, backslashes, control characters, and . or .. segments because a
browser could otherwise resolve a different path than Farm's server router.
Trailing slashes
Application page URLs omit a trailing slash by default. Set trailingSlash: true to generate
framework links with a slash and redirect matching page requests to that canonical URL in both
development and production:
export default defineConfig({
trailingSlash: true,
});The redirect uses status 308 and preserves the query string. The root URL remains /, and API,
integration, image, and metadata routes keep their own URL contracts. A <Link trailingSlash={false}>
or <Link trailingSlash> prop overrides the app default for that link.
API client base URL
Farm's typed API client uses the current origin and /api by default. Configure api when the
browser should call a different origin or path:
import { defineConfig } from "@farm.js/core";
export default defineConfig({
api: {
baseURL: ({ mode }) =>
process.env.GITHUH_API_URL ?? (mode === "development" ? "http://127.0.0.1:8080" : undefined),
basePath: "/api",
},
});An origin-only baseURL, such as https://api.example.com, is joined with basePath. If
baseURL already contains a path, such as https://api.example.com/v1, that path is the API root
and basePath is ignored. Both fields accept a string or a sync/async resolver receiving
{ root, mode, env }. Farm resolves the function during configuration and only embeds the resulting
public URL in the browser bundle.
A root-relative API root is also mounted by Farm in development and production. For example,
api: { basePath: "/v2/api" } makes a route declared at app/api/users/route.ts available at
/v2/api/users. Farm treats an absolute baseURL as external and does not remount the current
application's API routes for it. basePath rejects backslashes, control characters, and . or
.. segments, including encoded path separators. Root-relative baseURL and basePath values
must begin with one slash; network-path references such as //api.example.com are rejected so the
server mount and browser URL cannot resolve to different origins or paths.
The option configures the HTTP apiClient returned by createApiClients() automatically
(and the existing createAPIClient() factory). The paired server api always uses the local app's
registered routes and server mount, never an external browser API origin. An explicit per-client baseURL still
takes precedence.
This also applies to RSC builds and their Nitro servers, including apps configured with
defineConfig from @farm.js/plugin/rsc. API requests at the custom prefix stay on the API
pipeline rather than being decoded as server actions. The canonical /api routes remain available;
an external API URL changes the client destination only, not the local server mount.
MCP transport
Install @farm.js/mcp, then configure one authenticated transport directly—no plugin array is
needed:
import { defineConfig } from "@farm.js/core";
export default defineConfig({
mcp: {
authorize: async ({ request }) => {
const session = await getSession(request);
return session ? { subject: session.user.id } : false;
},
},
});This mounts /api/mcp. Typed API routes are not exposed as MCP tools unless their endpoint config sets
mcp: true or supplies MCP metadata, or you explicitly select endpoint instances in mcp.tools.
authorize receives { request, tool, tools, server }; return { subject, tools: ["tool_name"] }
to limit both discovery and invocation for that caller. You can also add standalone defineTool()
definitions from @farm.js/mcp to the same list; these validate their own input and receive the
authorized principal without creating separate HTTP routes. Optional outputSchema validates
standalone results; endpoint-backed tools reuse a route factory's output validator. Both advertise
the validated result shape to MCP clients. See API MCP for
mixed declarations, the resolved catalog, and permission checks.
Agent readiness
agent holds features that help AI agents and crawlers understand a public site, and decide which
of them may use it. The two that only describe what agents can already read are on by default:
jsonLd (a schema.org WebSite built from your metadata) and llmsTxt (an index of your static
pages, when the docs engine isn't serving its own). The rest, which expose more
or change who may crawl, are opt-in. Turn a default off with false, for example on an internal tool:
export default defineConfig({
agent: { jsonLd: false, llmsTxt: false },
});Pages without Open Graph metadata also get og:title and og:description from their own title and
description, so social cards and agents citing a page have something to show.
import { defineConfig } from "@farm.js/core";
export default defineConfig({
agent: {
llmsTxt: {
title: "Acme",
summary: "Billing for small teams.",
exclude: ["/admin/[...path]"],
},
noindexPreviews: true,
jsonLd: true,
crawlers: { search: "allow", training: "block" },
apiCatalog: true,
},
});noindexPreviews: true adds X-Robots-Tag: noindex, nofollow to every response from Farm's server
on a preview deployment; Preview deployments covers how
Farm detects one.
Left unset, llmsTxt serves /llms.txt without /llms-full.txt (which renders every page); with
the docs engine enabled it stays off, since the docs engine answers /llms.txt itself.
llmsTxt: true serves /llms.txt: a Markdown index of every static page,
with each page's metadata title and description, linking to its Markdown mirror
when one is exposed. An options object sets the title and summary (the root layout's metadata by
default), adds details Markdown, and narrows the list with include and exclude route patterns,
which use the same syntax as md.expose. Dynamic routes are left out because they have no single
URL.
It also serves /llms-full.txt: the same header, then a block per listed page with its title, URL,
and description, separated by ---. Pages with an exposed Markdown mirror get it inlined; a page
without one keeps its block but no body. Pages are read with a fresh request that carries no cookies
or credentials, so a page behind auth is left out rather than copied into a file anyone can fetch.
full: false turns off the generated /llms-full.txt (a public/llms-full.txt or llms-full.ts
still serves that path). Rendering llms-full.txt renders every listed page. The production server shares one
render between concurrent requests and runs one at a time, so a burst of requests does not multiply
page renders. Set revalidate (seconds) to reuse a render on the server for that long and let a CDN
cache both generated files. A page whose Markdown takes longer than 30 seconds keeps its block
without a body, and that render is not reused, so the next request reads the page again.
Each file can be overridden on its own. A static public/llms.txt or public/llms-full.txt is
served as-is. An llms.ts or llms-full.ts metadata route
replaces the generated file with whatever it returns, either the complete file as a string or the
structured format, and receives the generated pages and defaults to build on. When the
docs engine is enabled too, the app's own files take /llms.txt and
/llms-full.txt.
jsonLd adds a schema.org entity to page heads, built from the site's metadata: the site name or
title, metadataBase, and description. A page with none of those gets no JSON-LD. Left unset, the
entity is a WebSite; true makes it an Organization, and an object sets the type (emitted as
@type) and fields such as name, url, logo, and sameAs.
Pages and layouts add their own structured data with metadata.jsonLd, an object or an array of
objects. It works without agent.jsonLd; when both are set, the site script comes first.
import type { MetadataProps } from "@farm.js/core";
export async function generateMetadata({ params }: MetadataProps<"/blog/[slug]">) {
const post = await getPost(params.slug);
return {
title: post.title,
jsonLd: {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
datePublished: post.publishedAt,
},
};
}Each object renders its own <script type="application/ld+json"> with < escaped, so values
cannot close the tag. Entries from layouts render first and accumulate with the page's instead of
replacing them. Client navigation swaps page and layout JSON-LD for the next route's and leaves
the site script and any ld+json script the app renders itself in place.
Instead of writing those objects by hand, use the typed builders in @farm.js/core/json-ld:
article, breadcrumbList, faqPage, product, and softwareApplication. Each returns a plain
object with @context and @type set and unset fields left out. Pass one, or an array of them, as
metadata.jsonLd:
import type { Metadata } from "@farm.js/core";
import * as jsonLd from "@farm.js/core/json-ld";
export const metadata: Metadata = {
jsonLd: [
jsonLd.article({
type: "TechArticle",
headline: "Routing",
datePublished: new Date("2026-10-01"),
author: { name: "Acme", type: "Organization" },
}),
jsonLd.breadcrumbList([{ name: "Docs", url: "https://acme.test/docs" }, { name: "Routing" }]),
],
};Values are passed through as written, so use absolute URLs. Spread a result to add schema.org
properties the builder does not model, for example { ...jsonLd.product(input), gtin13: "..." }.
AI crawlers
crawlers serves a generated /robots.txt with a group per category you set:
| Option | User agents |
|---|---|
search | OAI-SearchBot, ChatGPT-User, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User |
training | GPTBot, ClaudeBot, Google-Extended, CCBot, Applebot-Extended, Meta-ExternalAgent, Bytespider |
"allow" writes Allow: / for that group and "block" writes Disallow: /. A category you leave
out gets no group, so those agents follow the User-agent: * rules. search covers agents that
fetch pages to cite or link them in answers, including ones acting on a user's request, which some
vendors say may not follow robots.txt. training covers crawlers and tokens that decide whether
content trains models; blocking them does not affect search engines such as Googlebot.
The file ends with User-agent: * / Allow: /, then a Sitemap: line when the app has a
sitemap.ts and the root layout sets metadataBase. Farm does not build that URL from the request's
Host header, since CDNs cache robots.txt. crawlers: true serves only those default lines.
export default defineConfig({
agent: {
crawlers: {
search: "allow",
training: "block",
// Written after the generated groups. A `*` rule replaces the default allow-all group.
rules: [{ userAgent: "*", allow: "/", disallow: ["/admin/", "/api/"] }],
// Absolute URLs. Replaces the sitemap.ts default; [] leaves the line out.
sitemap: ["https://acme.test/sitemap.xml"],
},
},
});Crawlers combine groups that name the same agent, so a rule for GPTBot with allow: "/blog/"
next to training: "block" lets GPTBot read /blog/ only. A named group replaces the * group for
that agent, so the * rule's disallow paths do not apply to agents in an "allow" group.
The app's own file wins. Farm serves /robots.txt from, in order:
public/robots.txt, served as-is.- A root
robots.tsmetadata route insrc/app. - The generated file from
agent.crawlers. - The docs engine's robots.txt, when the docs engine is enabled.
API catalog
apiCatalog: true serves /.well-known/api-catalog (RFC 9727),
so an agent that knows only your origin can find your APIs. It is a Linkset
(application/linkset+json) listing:
- the HTTP API, with its OpenAPI spec as
service-descand the API reference asservice-doc, whenopenapiis enabled; - the MCP endpoint, when the top-level
mcpconfig is enabled.
{
"linkset": [
{ "anchor": "/.well-known/api-catalog", "item": [{ "href": "/api" }, { "href": "/api/mcp" }] },
{
"anchor": "/api",
"service-desc": [{ "href": "/openapi.json", "type": "application/vnd.oai.openapi+json" }],
"service-doc": [{ "href": "/docs/reference", "type": "text/html" }]
}
]
}Links are root-relative, so they resolve against the catalog URL and follow the basePath. A HEAD
request returns the Link: </.well-known/api-catalog>; rel="api-catalog" header the RFC requires.
Listing the MCP endpoint does not open it: its authorize check still applies. An MCP server added
through apiMcp() in plugins is not listed; use the top-level mcp config.
Isolated client hydration
React applications can keep using "use client" without enabling React Server Components. By
default, Farm preserves its compatible route-wide hydration behavior. The isolated hydration
experiment lets an otherwise server-rendered page or layout ship and hydrate eligible client leaves
instead of the complete route module:
import { defineConfig } from "@farm.js/core";
export default defineConfig({
experimental: {
isolatedClientHydration: "enabled",
},
});The option has three modes:
| Mode | Behavior |
|---|---|
"off" | Keeps route-wide hydration. This is the default. |
"analyze" | Reports eligible boundaries without changing emitted code or runtime work. |
"enabled" | Hydrates safe client leaves independently and keeps unsupported routes safe. |
The mode applies to synchronous pages and layouts, which have a route-wide alternative. An async
page or layout has none, so its eligible client components always hydrate as islands, in every
mode. See Async pages and client components.
An eligible boundary is a local, statically analyzable "use client" module with a default or
named capitalized component export and serializable props. Farm preserves its server-rendered HTML,
emits the client component as a separate browser chunk, and hydrates that leaf as its own React
root. Sibling leaves receive independent roots. Client components imported by another client
component stay in their parent's root, so Farm never creates overlapping roots for one client
graph. Props travel in an HTML-safe, non-executable JSON payload. Plain objects, arrays, strings,
booleans, finite numbers, and null are supported.
Package boundaries, re-export graphs, ambiguous exports, React-element children, functions, symbols, class instances, circular objects, and routes that still require shared React context keep the route-wide path when Farm can identify them statically. If an unsupported value is discovered only while rendering, Farm preserves the SSR output and leaves that boundary inert with a development diagnostic. Module-load and root-render failures likewise restore the original server HTML and report the boundary reference plus the original error.
An integration provider is route-wide by default because an independent root cannot inherit its
context. A provider that is safe to instantiate around every isolated root can declare
supportsIsolatedHydration: true; otherwise Farm retains route-wide hydration for the app.
Farm also applies a measured graph-cost guard. Up to four statically bounded isolated roots can use the isolated plan. A page or layout with a larger client graph stays on route-wide hydration and prints the owner, detected count, and limit. Lists whose boundary count depends on runtime data also stay route-wide because Farm cannot prove their root cost before streaming. In the maintained 40-sample Chrome browser benchmark, eight independent roots were the first stress shape to exceed the route-wide hydration budget. See the raw samples and full cost table. The same report includes compiler-enabled route-wide and isolated controls. It verifies that every measured leaf actually compiled, then checks initial hydration and repeated state updates independently.
SPA navigation preserves isolated roots that live in a shared layout, including their state and DOM identity. Farm unmounts roots in the outgoing route subtree before replacing it, then hydrates only the boundaries introduced by the incoming fragment. Superseded navigation work is aborted before it can hydrate stale HTML.
This flag does not enable RSC, change the meaning of "use client", or make Server Components part
of the wire format. When experimental.serverComponents is enabled, the RSC transport remains the
owner and Farm ignores isolated client hydration. Treat "enabled" as an experimental performance
option and measure the route's client JavaScript and interaction cost before adopting it broadly.
The maintained benchmark includes equivalent RSC controls rather than assuming the non-RSC path is
faster.
Partial Prerendering
Partial Prerendering (static-shell caching) is experimental and disabled by default. Enable it
app-wide with experimental.ppr, then opt individual routes in with export const ppr = true,
the Next-compatible export const experimental_ppr = true, or a "use ppr" directive. Route
declarations are inert while the flag is off, and those routes render fully dynamically.
import { defineConfig } from "@farm.js/core";
export default defineConfig({
experimental: {
ppr: true,
},
});See Cache and PPR for shell caching, Suspense holes, invalidation, and observability events.
Images
Farm optimizes local and allowlisted remote images through the same runtime on development and production deployments.
import { defineConfig } from "@farm.js/core";
export default defineConfig({
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.example.com",
pathname: "/catalog/**",
},
],
qualities: [75, 90],
formats: ["image/avif", "image/webp"],
maximumResponseBody: "10mb",
},
});Remote sources are denied by default. See Images for static imports, responsive layouts, provider selection, caching, and security behavior.
Preload budgets
Farm keeps one image preload—the explicitly high-priority hint first—and two font preloads by
default. Lower-priority hints above those budgets are removed from buffered HTML and Link response
headers, while the actual image and font elements remain unchanged and load normally. Route scripts,
stylesheets, and module preloads are not removed. Budgets count resources, not hints: a URL preloaded
in both the HTML and the Link header counts once, and its hints are kept or removed together.
Preload-looking text inside comments, scripts, and other inert content is left unchanged, including
when surrounding Unicode text changes length during case conversion.
Production responses reuse buffered HTML without a body decode/re-encode when Farm can prove it
contains no preload candidates. Link header budgets still apply, and HTML transformed by plugins
is checked after those transforms. Candidate-bearing documents retain the full combined budget.
import { defineConfig } from "@farm.js/core";
export default defineConfig({
performance: {
preload: {
mode: "enforce",
maxImages: 1,
maxFonts: 2,
},
},
});Farm prints one actionable warning when a route exceeds a budget. Use mode: "warn" to audit an
existing application without removing any hints. Mark the likely LCP image with preload (or
fetchPriority="high") and set preload: false on font declarations that are not needed above the
fold.
Layers
Use extends to compose ordinary Farm-shaped directories and packages. Entries apply from left to right, and project files and configuration have final priority.
export default defineConfig({
extends: ["@company/farm-base", "./layers/commerce"],
});A layer may contain an optional plain farm.config.ts plus its own src/app, components, middleware, APIs, and programmatic routes. It does not use a separate layer registration function. See Layers for package structure, merge rules, aliases, generated types, and override behavior.
Content Security Policy
Configure an app-wide Content Security Policy under security.csp. Farm applies it to pages, API responses, and pre-rendered output through the same response-header pipeline in development and production.
import { defineConfig } from "@farm.js/core";
export default defineConfig({
security: {
csp: {
directives: {
defaultSrc: ["'self'"],
baseUri: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
formAction: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "blob:"],
fontSrc: ["'self'", "data:"],
connectSrc: ["'self'", "https:", "wss:"],
},
},
},
});Directive names may use camelCase or kebab-case. Farm rejects duplicate normalized names, newlines, and directive values containing semicolons so configuration cannot accidentally create a second policy directive.
Use report-only mode while auditing an existing application:
security: {
csp: {
reportOnly: true,
directives: {
defaultSrc: ["'self'"],
reportTo: ["csp-endpoint"],
},
},
}You can also pass an already serialized policy as csp: "default-src 'self'; object-src 'none'". The longer contentSecurityPolicy config name is intentionally unsupported; use csp.
Farm emits small inline hydration, theme, and route-state bootstraps. The first example uses the
compatibility path, so it permits inline scripts. For a strict script policy, enable Farm's managed
script authorization and remove 'unsafe-inline':
security: {
csp: {
nonce: true,
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
},
},
}For dynamic HTML, Farm generates a fresh nonce for every response, adds it to the directive that
governs script elements (script-src-elem, then script-src, then default-src), and stamps every
script element in the streamed document. Any existing script nonce attribute is normalized to the
fresh response nonce so application-authored inline scripts follow the same policy.
Script contents, including Unicode text, are preserved across streamed response chunks.
For fully prerendered HTML, the same option keeps the page static. Farm removes the build-time nonce,
hashes the exact contents of every inline script with SHA-256, and emits a route-specific policy with
those hashes. External scripts still need their origin in script-src or script-src-elem. Dynamic
responses and PPR shells continue to use fresh nonces, so a nonce is never cached or reused. Keep
reportOnly on while auditing and verify every third-party script and connection before enforcing the
policy.
Without nonce: true, Farm warns when a configured policy would block its inline framework scripts.
Server HTTP policy
Farm applies one request-body limit to API routes, integrations, workflow HTTP triggers, and uploads handled by those surfaces. The default is 10 MB.
import { defineConfig } from "@farm.js/core";
export default defineConfig({
server: {
bodySizeLimit: "10mb",
trustProxy: false,
headersTimeout: "60s",
requestTimeout: "5m",
keepAliveTimeout: "5s",
gracefulShutdownTimeout: "30s",
health: {
livenessPath: "/_farm/health/live",
readinessPath: "/_farm/health/ready",
},
},
});Farm checks Content-Length when present and also counts the received bytes, so chunked requests cannot bypass bodySizeLimit. Oversized requests receive 413 Payload Too Large before the route or integration handler runs. Server Actions keep their separate, tighter serverActions.bodySizeLimit setting.
Body rejection initiates stream cancellation without waiting for producer cleanup. This also applies to cloned requests: an unread original body cannot delay the rejection, and a cleanup failure does not replace the 413 response.
The RSC development bridge applies these limits too. POST, PUT, PATCH, DELETE, and QUERY
bodies keep their original bytes, including multipart uploads and binary data. GET and HEAD
remain bodyless. Action origin validation runs before buffering an action body.
trustProxy defaults to false. Enable it only when the app is behind a trusted reverse proxy that removes client-supplied forwarding headers and writes its own X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto values. Farm uses those headers for the client address and public request URL only when the proxy is trusted. A directly exposed Farm server must leave it disabled so a client cannot spoof the address or authority used by rate limits, redirects, authentication callbacks, logs, or access policy.
Workflow runner secrets are accepted only through Authorization: Bearer <secret> or X-Farm-Workflow-Secret. Farm does not accept secrets in query strings because URLs are commonly retained in logs, browser history, and referrer data.
The long-running Node adapter applies headersTimeout, requestTimeout, and keepAliveTimeout to its HTTP server. headersTimeout limits how long a client can occupy a connection while sending headers, and requestTimeout limits receipt of the complete request. These are transport timeouts, not limits on route-handler or database execution. Durations accept milliseconds or strings such as "15s", "2m", and "1h".
On SIGTERM or SIGINT, Node output immediately fails readiness, stops accepting connections, drains active responses and streams through Nitro, and then runs Farm integration and plugin cleanup. gracefulShutdownTimeout is the maximum drain period before remaining connections are forced closed. The process starts plugin and integration runtime state before it begins listening, so a successful readiness response means startup completed.
Farm exposes two non-cacheable production health handlers by default:
GET /_farm/health/livereports whether the process is alive. It stays successful while the process drains.GET /_farm/health/readyreports whether the instance should receive traffic. It returns503before startup completes and after shutdown begins.
Customize both paths through server.health, or set health: false when an adapter supplies its own probes. Long-running Node output guarantees the shutdown sequence. Request-driven serverless and edge environments may not expose a reliable process shutdown event, so cleanup there remains platform-specific and must not be required for data correctness.
Server action security
Server actions are same-origin application RPC endpoints. Farm rejects cross-origin action requests by default and limits the encoded request body to 1 MB.
Server actions run on the React Server Components pipeline from @farm.js/plugin/rsc (see the
rsc-demo example). The
experimental flags below configure that pipeline; on their own they do not make server functions
callable from the browser, and Farm keeps rejecting client imports of server-function modules.
import { defineConfig } from "@farm.js/core";
export default defineConfig({
experimental: {
serverComponents: true,
serverActions: true,
},
serverActions: {
allowedOrigins: [],
bodySizeLimit: "1mb",
},
});allowedOrigins adds trusted origins when a reverse proxy or multi-origin deployment makes the browser origin differ from the server request origin. Entries can be exact origins, hosts, or leftmost-subdomain wildcards:
serverActions: {
allowedOrigins: [
"https://app.example.com",
"proxy.internal:8443",
"https://*.preview.example.com",
],
}Do not use allowedOrigins as a replacement for CORS or as a public API allowlist. Browser action requests must provide a matching Origin or Referer; Farm accepts Sec-Fetch-Site: same-origin when both are unavailable. Explicitly configured origins can cross a trusted proxy boundary.
bodySizeLimit accepts bytes or strings such as "500kb", "2mb", and "2MiB". Farm checks Content-Length when present and also counts streamed bytes, so chunked requests cannot bypass the limit.
When streamed action input exceeds the limit, Farm cancels it without awaiting producer cleanup or another branch of a cloned request. Cleanup errors do not replace the action's 413 rejection.
Rejected requests use generic, non-cacheable responses: 403 for origin failures, 413 for oversized bodies, and 415 for unsupported content types. Detailed parsing or execution errors stay in server logs.
Next-style route exports
Farm route modules can expose compact rendering options directly on the page when the behavior belongs to that route.
export const dynamic = "force-static";
export const revalidate = 60;
export default async function BlogPage() {
return <main>...</main>;
}Route rules
Farm's redirects(), rewrites(), and headers() config functions use the same source pattern
syntax. :name captures one path segment, while :name* and plain * capture the remaining
characters. Redirect and rewrite destinations can reuse named captures or use numbered captures
such as $1. Captured path segments are decoded and safely re-encoded before interpolation;
empty segments in catch-all captures are removed consistently in development and production. All
other source characters are matched literally. Sources must be pathname patterns beginning with
/; query strings and hashes belong in redirect or rewrite destinations and are rejected in
sources because matching operates on the request pathname.
For redirects and rewrites, the incoming query string is preserved when the destination has no
query. A query written in the destination replaces the incoming query string.
Rewrites use after-files semantics in development and production: an existing Farm page, API,
integration, docs, image, or metadata route wins, and the rewrite is considered only as a fallback.
Configured response headers are applied after route handlers in both modes, so they win when the
same header is returned by a handler. Link is additive: handler and configured link values are
merged instead of replacing one another. Set-Cookie is also additive, and each handler or
configured cookie remains a separate response header.
export default defineConfig({
async redirects() {
return [{ source: "/old/:path*", destination: "/new/:path*", permanent: true }];
},
});Redirect statusCode accepts only HTTP redirect statuses 301, 302, 303, 307, or 308.
Use permanent: true for the default permanent 308; otherwise Farm defaults to temporary 307.
Invalid values fail config resolution instead of producing a non-redirect response with a Location
header.
Use routeRules when behavior belongs to a URL pattern instead of one page file. Rules are normalized into Farm redirects/headers and passed to Nitro route rules for production adapters.
import { defineConfig } from "@farm.js/core";
export default defineConfig({
routeRules: {
"/": { prerender: true },
"/blog/**": { swr: 3600 },
"/admin/**": { render: "dynamic" },
"/api/**": { cors: true },
"/assets/**": {
headers: {
"Cache-Control": "public, max-age=31536000, immutable",
},
},
"/old": { redirect: "/new" },
},
});render: "static" maps to prerendering. render: "dynamic" forces a dynamic response. swr and isr accept true or a TTL in seconds. cors: true applies permissive API CORS headers; pass an object when you need a specific origin, methods, or headers.
API routes remain same-origin by default in development and production. Add cors only to route patterns that intentionally form a cross-origin browser API.
Rules can also provide runtime, regions, and maxDuration defaults. File pages, API routes, and layouts can override them with named exports. See Route Runtime for inheritance and deployment behavior.
Prefer route-level exports when one page owns the behavior. Prefer routeRules for broad groups, deployment-facing cache policy, API CORS, static asset headers, and legacy redirects.
Minimal project layout
Farm keeps the base project small:
farm.config.ts
src/
app/
page.tsxAdd optional files only when the app needs them:
docs.config.ts # Optional split for a large docs configuration
docs.json # Optional serializable docs configuration
src/app/api/**/route.ts
src/app/**/middleware.ts
src/lib/integrations.tsCron in config
Cron entries keep timing policy in farm.config.ts while application work stays in an ordinary API route.
export default defineConfig({
cron: {
dailyCleanup: {
schedule: "0 2 * * *",
path: "/api/maintenance/cleanup",
},
},
});See Cron for route protection, local commands, UTC syntax, deployment behavior, and reliability boundaries.
Integrations in config
import { defineConfig } from "@farm.js/core";
import { stripe } from "@farm.js/stripe";
import { supabase } from "@farm.js/supabase";
export default defineConfig({
integrations: {
billing: stripe({
secretKey: process.env.STRIPE_SECRET_KEY,
}),
auth: supabase({
url: process.env.SUPABASE_URL,
anonKey: process.env.SUPABASE_ANON_KEY,
}),
},
});The keys become typed namespaces. billing becomes api.billing, and auth becomes api.auth.
One-shot migrations
Use migrations.commands when the app needs a predictable command before build or deploy. This keeps schema setup close to the database and integration config without turning the framework into a migration engine.
import { defineConfig } from "@farm.js/core";
export default defineConfig({
migrations: {
commands: [
"pnpm drizzle-kit migrate",
{
name: "integration schema",
command: "farm generate --orm sqlite --output ./farm-integrations.sql",
env: {
FARM_SCHEMA: "integrations",
},
},
],
},
});Run them with:
farm migrateEach command runs from the project root unless it sets cwd. Commands run in order and the CLI stops on the first failure.
Production-site telemetry
Farm production server runtimes automatically detect their public HTTPS origin from incoming requests and report it to Farm's production-sites dashboard. No URL configuration is required. To disable this product telemetry for a deployment:
export default defineConfig({
telemetry: false,
});Farm schedules a small check-in after the first non-health production request and never waits for it before returning the application response. Only the detected origin, Farm version, renderer, and deployment target are sent. See Product telemetry for validation, privacy, retention, preview-environment, opt-out, and static-export details.
Farm Infra analytics
An app whose server environment sets FARMJS_API_KEY and FARMJS_PROJECT_ID reports request and MCP
tool analytics to that Farm Infra project. Set analytics: false to never send them. See
Farm Infra analytics for what is sent and how.
Deployment config
export default defineConfig({
deploy: {
target: "vercel",
outputDir: ".vercel/output",
},
});deploy.target selects the deployment provider. Farm resolves that to the matching Nitro preset and output shape unless you override it.
Deployment identity
Farm assigns one deployment ID to the server and browser output so requests from an older open page can be detected safely.
export default defineConfig({
deploymentId: process.env.RELEASE_ID,
});When deploymentId is omitted, Farm checks FARM_DEPLOYMENT_ID, VERCEL_GIT_COMMIT_SHA, and CF_PAGES_COMMIT_SHA, then calls generateBuildId for production builds. Development uses "development".
For a custom build ID, return one stable value for every instance of the same release:
export default defineConfig({
generateBuildId: async () => process.env.GIT_SHA || `build-${Date.now()}`,
});Prefer a CI release or commit identifier when a deployment runs on multiple servers. See Deployment for mismatch behavior.
Production notes
- Production route discovery disables filesystem watching, including when
vite.server.watchis configured. That option still applies to development; a one-shot build does not need a live watcher. - Keep secrets in environment variables, not committed config.
- Use
storage.driverandstorage.mountsfor KV data read throughgetStorage(). - Use a raw object at
storage.clientonly when schema-backed integrations need a database client; see Database and ORM Clients. - Use
migrations.commandsfor schema setup that should be explicit in CI. - Use
docs.entrywhen the docs runtime should be mounted automatically. - Prefer route-level exports such as
dynamic,revalidate, andpprwhen behavior belongs to one page. - Prefer
routeRulesfor broad URL patterns and platform-level cache/header behavior. - Keep
serverActions.allowedOriginsempty unless the deployment has a known proxy-origin mismatch. - Give every rolling release one stable
deploymentId; do not generate a different value per server instance. - Treat every server action as a public endpoint and authorize the current user inside the action or middleware.
- Keep
farm.config.tsas the single control plane instead of spreading framework behavior across many root files.