# Farm.js Docs > Complete Farm.js framework documentation. ## Post-response Work URL: /docs/after Schedule short server work with after() without delaying the response. # Post-response Work Use `after()` for short server work that should start once Farm has finished the current response. Typical uses include analytics, audit events, cache warming, notifications, and non-critical cleanup. Import it from the server-only entry: ```ts import { after } from "@farm.js/core/after"; ``` ## Use it in a page `after()` does not add its callback to the page's response time. ```tsx import { after } from "@farm.js/core/after"; import { recordPageView } from "@/lib/analytics"; export default async function ProductPage({ params }) { const product = await getProduct(params.id); after(async () => { await recordPageView({ productId: product.id }); }); return ; } ``` Farm waits for a streamed response body to finish before starting the callback. Redirect and not-found responses use the same request lifecycle. Cancelling a Web response also finishes that lifecycle, even if the stream producer's cleanup is still pending. Farm starts `after()` callbacks and stops counting the response as active; the caller of `cancel()` still receives the producer's cleanup promise, including any rejection. ## Use it in an API route Keep work required for correctness in the handler. Schedule only the follow-up work that may happen after the client receives success. ```ts import { after } from "@farm.js/core/after"; import { createEndpoint } from "@farm.js/core/api"; import { z } from "zod"; export const POST = createEndpoint( { method: "POST", body: z.object({ orderId: z.string() }), }, async ({ body }) => { const order = await confirmOrder(body.orderId); after(async () => { await sendReceipt(order.id); await recordOrderAnalytics(order.id); }); return { ok: true, order }; }, ); ``` The same API works in layouts, request middleware, server functions, and form actions because Farm creates one post-response queue for the whole request. ## Behavior - `after(callback)` returns `void`; Farm owns when the callback starts. - Callbacks start after the response finishes. For streaming responses, that means after the complete body is sent or the stream closes. - Callbacks run one at a time in registration order. - A callback may call `after()` again. The nested callback joins the end of the current queue. - A callback error is logged, does not change the response, and does not prevent later callbacks from running. - Request-local async context active at registration is preserved for the callback. - Calling `after()` outside an active server request throws a clear runtime error. On serverless adapters, Farm registers the queue with the provider's `waitUntil` lifecycle. On a Node server, Farm uses the response `finish` and `close` events. This keeps supported invocations alive without making the browser wait for the callback. If the Node response has already ended or been destroyed when Farm's middleware starts, Farm still completes the queue and removes its response listeners without waiting for another event. ## Best practices Capture stable values before scheduling work: ```ts const userId = session.user.id; const eventId = crypto.randomUUID(); after(() => analytics.track({ eventId, userId, name: "checkout.completed" })); ``` Do authentication, authorization, validation, and the primary database write before returning the response. The response is already committed when an `after()` callback runs, so the callback cannot safely change its status, headers, or cookies. Make callbacks idempotent when they call external services. A deployment runtime may stop unexpectedly, and `after()` does not add durable storage, retries, or exactly-once delivery. Use [Cron](/docs/cron) for periodic route invocation. Use the [Jobs Integration](/docs/integrations/jobs) for durable retries, long-running work, queues, visibility, or recovery. Use `after()` for bounded request follow-up work where low latency matters more than durable execution. --- ## API Client URL: /docs/api-client Call app API routes with apiClient.hello.get style inference, cache policies, invalidation, retries, callbacks, and optimistic updates. # API Client Call app API routes with apiClient.hello.get style inference, cache policies, invalidation, retries, callbacks, and optimistic updates. ## Create both callers once **src/lib/api.ts** ```ts import { createApiClients } from "@farm.js/core/api/client"; import { apiRoutes, type APIRouter } from "./api.generated"; export const { api, apiClient } = createApiClients({ routes: apiRoutes }); ``` `@farm.js/core/api/client` is the renderer-neutral entry for typed API callers. The existing `@farm.js/core/client` export remains supported for React apps that also use Farm's client hooks. Define each endpoint once in a file route or plugin. This shared module imports only generated paths/methods and types, never server handlers or credentials. Import `api` in server code and `apiClient` in browser code; do not create another caller in either component. Both app-route callers return `{ data, error, key }` and preserve the same input, output, method, and dynamic-parameter inference. Keep secrets out of this shared module, including its options. If the app also has integrations, add `AppIntegrations` as the second type argument to this same factory. You do not need a second caller setup. See [Integration callers](#integration-callers). The HTTP client uses the current origin and `/api` by default. To point every default client at another API, configure it once in `farm.config.ts`: ```ts import { defineConfig } from "@farm.js/core"; export default defineConfig({ api: { baseURL: () => process.env.API_ORIGIN, basePath: "/api", }, }); ``` `https://api.example.com` becomes `https://api.example.com/api`. A URL that already has a path, such as `https://api.example.com/v1`, uses that path directly. `baseURL` and `basePath` may be sync or async resolver functions; Farm evaluates them during config resolution and embeds only the resulting public URL. For a cross-origin API that uses cookies or HTTP authentication, pass the browser fetch credential mode when creating the client: ```ts export const { api, apiClient } = createApiClients({ baseURL: "https://api.example.com/v1", credentials: "include", }); ``` Farm forwards `credentials` to every route request from that client. The API must also allow the calling origin and credentialed requests through its CORS policy. ## Header defaults Instance `headers` accepts a string-valued object or a function returning one. The function can be synchronous or asynchronous; Farm calls it when an operation is called, not when the client is created. ```ts import { createApiClients } from "@farm.js/core/client"; import { apiRoutes, type APIRouter } from "./api.generated"; export const { api, apiClient } = createApiClients({ routes: apiRoutes, headers: () => ({ "Accept-Language": typeof document === "undefined" ? "en" : document.documentElement.lang || "en", }), }); // This call overrides the instance language without changing later calls. await apiClient.hello.get({ headers: { "accept-language": "fr" } }); ``` Use `headers: async () => ({ ... })` when obtaining defaults requires asynchronous work. The exported `ClientHeaders` type describes all three forms. Static objects remain supported; per-call `headers` remain objects, not resolver functions. Header names are case-insensitive. App routes merge forwarded server request headers first, then instance defaults, then per-call headers. Thus the example explicitly chooses English on the server; omit its `Accept-Language` default when you want `api` to preserve the incoming request's language. The existing server forwarding allowlist is unchanged. Farm snapshots the resolved defaults once per operation call, before cache lookup or dispatch. Cache hits still resolve headers; changed effective headers isolate the route client's cache and in-flight deduplication. Retries reuse the same snapshot. A thrown error, rejected promise, or invalid header produces the normal error result without fetching or returning cached data. These defaults also apply to `api.integrations` and `apiClient.integrations` when enabled. Providing `integrations.headers` replaces the shared header defaults for integration calls only; it does not merge the two resolvers. See [integration header defaults](/docs/integrations#header-defaults) for integration-specific precedence and separate server defaults. A resolver is not a server-only boundary. Anything imported into this shared module may reach the browser, and the resolver runs wherever its caller runs. Do not put provider credentials or private environment values here. Read request-specific values inside the resolver or a request-scoped server module, never by mutating a shared singleton with one user's credentials. ## Cancellation and deadlines Set `timeoutMs` on `createApiClients` for a default deadline, or override it per call: ```ts export const { api, apiClient } = createApiClients({ routes: apiRoutes, timeoutMs: 10_000, }); const controller = new AbortController(); const pending = apiClient.hello.get({}, { signal: controller.signal, timeoutMs: 2_000 }); controller.abort(); const result = await pending; // result.error.code is "aborted" (or "timeout" when Farm's deadline expires). ``` The budget starts at call time and includes asynchronous header resolution, transport, response decoding, and retry waits. `0` disables the deadline (also the default); accepted values are integers from `0` to `2147483647`. A per-call `0` disables an instance deadline. Keep `signal` per call, not on a shared long-lived instance. Cancellation returns `{ data: undefined, error, key }` with status `0`, stops retries, skips cached results when already aborted, and rolls back pending optimistic updates even when `rollbackOnError` is false. Calls with a signal or deadline do not participate in in-flight deduplication, so one caller cannot cancel another. Completed results can still use the cache. Background revalidation retains its call budget until it finishes. A returned stream is handed to the caller: the deadline ends at that handoff, not at the end of stream consumption; the caller's signal still reaches the underlying HTTP request. Local `api` dispatch combines the call signal with the incoming request signal. Handlers can pass `request.signal` to cancellable work. Farm stops waiting when cancelled, but a handler or custom transport that ignores the signal may continue running. Cancellation cannot undo completed writes or external side effects. These defaults also reach integration callers; `integrations.timeoutMs` overrides the shared deadline. See [integration cancellation](/docs/integrations#cancellation-and-deadlines). ## Custom HTTP transport Pass a fetch-compatible function when a caller needs an HTTP wrapper for testing or tracing: ```ts export const { api, apiClient } = createApiClients({ routes: apiRoutes, fetch: async (url, init) => { const response = await globalThis.fetch(url, init); return response; }, }); ``` The function has the same type as `globalThis.fetch` and receives Farm's resolved URL and `RequestInit`, including headers, credentials, body, and cancellation signal. Return a normal `Response` and forward the signal in wrappers. Farm still handles decoding, errors, retries, and deadlines. Without this option, Farm uses global fetch as before. Supply a bound function if your implementation requires a particular `this` value. This replaces HTTP only: `apiClient` uses it, but server `api` still dispatches app routes locally. Integration HTTP calls (including server HTTP fallback) inherit it; `integrations.fetch` overrides it for integrations. Registered local integration handlers do not use HTTP and are unchanged. App-route caches stay private to instances with a custom transport, even with `cache.scope: "shared"` or `credentials: "omit"`: wrappers can add identity that Farm cannot see. If a wrapper changes users internally, also reflect that identity in the caller's header resolver or create a new instance; Farm cannot detect hidden identity changes. ## Shared lifecycle hooks Set `onRequest`, `onResponse`, or `onError` on the caller instance for shared logging or error reporting. These are ordinary callbacks, not React hooks: ```ts export const { api, apiClient } = createApiClients({ routes: apiRoutes, onRequest(event) { console.debug(event.method, event.path); }, onResponse(_data, _error, event) { console.debug("Status:", event.status); }, onError(error) { console.error(error.message); }, }); await apiClient.hello.get( {}, { onError(error) { // Handle this call's error in the UI as well as shared reporting. console.debug("Could not load greeting:", error.message); }, }, ); ``` Shared hooks run first, then the corresponding per-call hook. Per-call route response types remain inferred. Shared hooks use `unknown` data because one instance covers many routes; `ClientLifecycleHooks`, `ClientRequestEvent`, and `ClientResponseEvent` are exported for reusable observers. Return values are ignored. Promises are not awaited, and thrown/rejected shared hooks are reported through `globalThis.reportError` (or console) without failing the call. Asynchronous work may finish out of order even though callbacks are invoked shared-first. `onRequest`/`onResponse` observe execution attempts, including retries and background refreshes, not cache hits. Deduplicated requests share attempt events. `onError` runs once on final failure for each logical call, including background failures; it does not run for each failed retry. An attempt can fail before HTTP dispatch, such as header resolution or cancellation. A response event observes a completed attempt; aborting inside it cannot retroactively cancel that result. These hooks do not add retries or replace existing per-call success, settlement, or status hooks. Shared defaults apply to integration callers too. `integrations.onRequest`, `onResponse`, and `onError` replace the corresponding shared default for integrations; per-call hooks still compose. Local server calls run their hooks on the server; HTTP callers run them where invoked. Keep shared observers browser-safe and avoid logging credentials, request bodies, or personal data. ## Call a route **Browser usage** ```ts const result = await apiClient.hello.post({ body: { name: "Ada" }, }); if (result.error) { console.error(result.error); } else { console.log(result.data.message); } ``` If a route path contains a lowercase HTTP method segment such as `get`, `post`, or `delete` that collides with a method on its parent route, the generated client exposes a leading-slash literal alias so the two cannot be confused: ```ts // Both src/app/api/users/route.ts and src/app/api/users/get/route.ts export GET. const result = await apiClient["/users/get"].get(); ``` The leading slash marks the whole key as a literal API path. This also works when the method-named segment is in the middle of a colliding route, for example `apiClient["/users/get/profile"].post(...)`. Non-conflicting paths keep their ordinary nested form. A typed `HEAD` route is called with `.head()`. Its result keeps the same `{ data, error, key }` shape, with `data` set to `undefined` because HTTP HEAD responses do not have a body. Array-valued query inputs use repeated URL parameters. For example, ```ts await apiClient.posts.get({ query: { tag: ["react", "vite"] } }); // GET /api/posts?tag=react&tag=vite ``` This is the same array representation that API route query schemas receive. Query values must be strings, numbers, booleans, bigints, or arrays of those scalar values; `null` and `undefined` entries are omitted. Nested objects are rejected instead of being silently encoded as `[object Object]`. Untyped callers may also pass `URLSearchParams` directly when they need its native ordering, duplicate-key, and encoding semantics. ## Scoped dynamic routes Once an app has API routes, `farm generate`, development startup, and production builds emit `apiRoutes` alongside `APIRouter` in `src/lib/api.generated.ts`. An app without API routes gets no such file. Pass that manifest to enable parameter resolution: ```ts import { createApiClients } from "@farm.js/core/client"; import { apiRoutes, type APIRouter } from "./api.generated"; export const { api, apiClient } = createApiClients({ routes: apiRoutes }); ``` For `POST /api/projects/[projectId]/uploads/[uploadId]`: ```ts const project = apiClient.projects.$params({ projectId: "project-123" }); const result = await project.uploads.post({ params: { uploadId: "upload-456" }, body: { title: "Design draft" }, }); if (result.error) throw result.error; console.log(result.data?.title); ``` `$params()` returns a reusable, immutable caller and sends no request. Only the explicit HTTP method executes the call. Bound parameters do not have to be repeated and cannot be overwritten by a descendant call. You can bind the final parameter as well: ```ts const upload = project.uploads.$params({ uploadId: "upload-456" }); await upload.post({ body: { title: "Updated draft" } }); ``` The generated types describe registered methods, required parameters, validated input, and JSON response data. The runtime manifest contains only paths and methods; importing it never imports the config, server handlers, validators, or credentials into the browser. Existing static clients without a manifest remain supported. Binding preserves position: `/files/[id]/versions` uses `apiClient.files.$params({ id }).versions.get()`, while `/files/versions/[id]` uses `apiClient.files.versions.get({ params: { id } })`. Catch-all parameters use arrays: `apiClient.docs.$params({ parts: ["guides", "start"] }).get()` for `/api/docs/[...parts]`. Optional catch-alls can bind an empty object. Intermediate parameters must be bound before accessing their children through the shorthand; existing bracket-pattern access remains available. The client encodes parameter values, respects the configured API base URL/path, and rejects missing/unknown parameters, unsafe values, unregistered methods, and ambiguous calls before fetching. A supplied `undefined` ID never falls back to a collection request. Static server routes still win: if `/api/uploads/stats` exists, calling `/api/uploads/[id]` with `id: "stats"` throws a shadowing error rather than calling the wrong endpoint. Choose non-conflicting IDs or paths. Resolved URLs participate in caching and invalidation, so different bound IDs remain separate. Bind the final parameter before passing a route reference to a hook or invalidation helper when you need one concrete resource; collection/detail overloads otherwise describe multiple inputs. Plugin paths containing HTTP method names use a literal alias such as `apiClient["/projects/get"].get()`. `$params` is reserved for the scope helper. Both callers returned by `createApiClients()` support these scopes. Use `api` for local calls during a Farm server request and `apiClient` for HTTP calls. For standalone HTTP calls from server code, give `apiClient` a trusted absolute `baseURL` and explicitly forward only the credentials the target needs. See [Server callers](#server-callers) for local-dispatch boundaries. ## Type-safe QUERY requests A route that exports `QUERY` becomes a `.query()` caller. Its body and response are inferred from the endpoint, just like the existing `.get()` and `.post()` callers: ```ts const result = await apiClient.products.search.query( { body: { filters: [{ field: "category", value: "tools" }], limit: 20, }, }, { cache: { policy: "stale-while-revalidate", staleTime: 30_000, }, }, ); if (!result.error) { // Inferred from the QUERY handler response. console.log(result.data.products, result.data.total); } ``` TypeScript reports an error if `body` is missing or a filter has the wrong shape. Farm sends the body as JSON and uses the `QUERY` method on the wire. Opt-in cache keys include the API origin, path, URL query parameters, request body, `Content-Type`, and `Content-Encoding`. Multipart QUERY requests need an explicit cache key because a generated multipart boundary cannot be represented reliably before `fetch` sends the request. Farm adds `Content-Type: application/json` when it serializes a JSON request body. Bodyless requests do not receive that header, and an explicitly configured content type takes precedence regardless of header casing. ## Upload files and consume progress streams `toFormData()` retains the endpoint's body shape while sending files as real multipart fields. When an endpoint returns `jsonStream()`, the generated client exposes a typed, single-consumer async iterable. Concurrent `next()` calls on the same iterator are served in order, including items already buffered in a transport chunk. Cancellation interrupts outstanding reads without waiting for another chunk; `cancel()` itself still waits for producer cleanup: ```ts import { toFormData } from "@farm.js/core/api"; const result = await apiClient.imports.post({ body: toFormData({ title: "Quarterly report", file, }), }); if (result.error) { throw result.error; } for await (const event of result.data) { if (event.phase === "accepted") { console.log(`Uploading ${event.bytes} bytes`); } else { console.log(`Imported ${event.imported} rows`); } } ``` Farm passes the `FormData` object directly to `fetch`, allowing the runtime to generate the required multipart boundary. Do not set `Content-Type` manually. Stream items are decoded only as the consumer advances the iterator, and `result.data.cancel()` aborts the response reader when the UI no longer needs progress. `jsonStream()` closes its source iterator at most once when cancelled or when a source/serialization error occurs. A pending source read that finishes after cancellation is discarded. Explicit cancellation waits for that cleanup and exposes cleanup failures; a source/serialization failure keeps its original error even if cleanup also fails. ## Track mutations in React `useMutation` gives generated API methods and Farm server functions the same pending, result, and error lifecycle. API methods keep using the typed HTTP client underneath; they are not converted into React Server Actions. ```tsx "use client"; import { useMutation } from "@farm.js/core/client"; import { apiClient } from "@/lib/api"; export function CreateProductButton() { const createProduct = useMutation(apiClient.products.post, { request: { invalidate: [[apiClient.products.get]], }, }); return ( ); } ``` Use `mutate` for event handlers and `mutateAsync` when later code needs the resolved value: ```ts const product = await createProduct.mutateAsync({ body: { name, category }, }); ``` The return value includes `data`, `error`, `variables`, `status`, `pending`, and `reset`. Pass the existing API-client cache, retry, invalidation, and optimistic options through `request`. A server-function target honors `request.retry`, `request.optimistic`, and `request.invalidate` with the same shapes; the remaining `request` options describe API-route transport and continue to apply to API routes only. Local `optimistic` state on `useMutation` is separate from an API cache update: it controls `mutation.data`, while `request.optimistic` updates shared cached queries. For a server-function target, optimistic updates and invalidations name explicit structured cache keys, since the shared cache is the surface a server function can reach: ```tsx const rename = useMutation(renameProduct, { request: { optimistic: { update: [[["product", id], (current) => ({ ...current, name })]], rollbackOnError: true, }, invalidate: [["product", id]], }, }); ``` The updater applies to the shared client cache before the server function runs, so a `useServerQuery` watching `["product", id]` renders the new name immediately. On success the update commits and the invalidation travels the shared invalidation bus, exactly like a server-declared `invalidates`; on failure `rollbackOnError` restores the previous entry, and without it the touched entry is marked stale. Route-reference tuples such as `[apiClient.products.get]` need an API caller's route identity and are skipped for server-function targets; server-side keys the client cannot know about belong in the server function's own `invalidates`. Each local optimistic callback receives the latest scheduled mutation data, even when multiple submissions occur before React rerenders. With `rollbackOnError: true`, a failed latest submission restores the snapshot captured immediately before that submission's optimistic update. ### Pause offline submissions By default a mutation dispatches regardless of connectivity and a submission with no network fails after its retries. Opt into offline awareness with `networkMode: "online"`: ```tsx const createProduct = useMutation(apiClient.products.post, { networkMode: "online", }); // createProduct.paused is true while the browser is offline; // the submission dispatches automatically on the `online` event. ``` While the browser reports offline, a new submission waits in a `paused` state instead of dispatching, and a dispatch that failed with a connectivity error while offline pauses and rides the next reconnect instead of surfacing that error. `pending` stays true and `status` stays `"pending"` for a paused submission; the new `paused` flag distinguishes "waiting for connection" from "on the wire" so the UI can say so. Paused submissions resume in submission order on the `online` event. `reset()` rejects paused submissions immediately rather than leaving them waiting, and a disowned submission never dispatches after a later reconnect. Optimistic state applied before the pause stays visible while waiting. The pause window is in-memory: a reload drops paused submissions, so keep them short-lived and surface `paused` to the user. `useFetcher` accepts the same option and exposes the same flag. The default `networkMode: "always"` preserves existing behavior exactly. For overlapping submissions, `status`, `data`, and `error` describe the latest submission. Older completions do not replace its result or run completion callbacks. `pending` separately tracks whether any submission is still running, so it can remain `true` after the latest one succeeds or fails. `useFetcher` follows the same rules. Mutation `onSuccess`, `onError`, and `onSettled` callbacks are non-awaited observers. A thrown error or rejected promise is reported through `reportError` (or `console.error` as a fallback), without changing the target result, rolling back a successful mutation, or decrementing pending work again. Use the awaited `mutateAsync` result for required follow-up work. If a callback resets the hook or submits again, the older submission does not run a stale `onSettled` callback. `reset()` clears the displayed mutation state; it does not cancel the underlying work. Calls started before reset still settle their own promises, but cannot change the new state or run its completion callbacks. If you submit again, `pending` counts only calls started after reset. `useFetcher.reset()` follows the same rule. ## Submit without navigation Use `useFetcher` when a button or form should run an operation without changing the current route. It accepts generated API methods, Farm server functions, and ordinary async functions: ```tsx "use client"; import { useFetcher } from "@farm.js/core/client"; import { apiClient } from "@/lib/api"; export function CreateProductForm() { const createProduct = useFetcher(apiClient.products.post, { request: { invalidate: [[apiClient.products.get]], }, }); return ( {createProduct.error ?

{createProduct.error.message}

: null} {createProduct.data ?

Created {createProduct.data.name}

: null}
); } ``` The fetcher exposes `state` (`idle` or `submitting`), `status`, `pending`, `paused`, `data`, `error`, `variables`, the active `formData`, `submit`, `submitAsync`, `Form`, and `reset`. It uses the same optimistic updates, rollback, callbacks, typed errors, and API-client request options as `useMutation`. Generated API forms map text fields to a JSON `{ body: ... }` by default, or `{ query: ... }` for GET/HEAD routes. For body methods, `` preserves the FormData body, including repeated fields and files. A submit button's `formEncType` can override the form encoding. Imperative `submit(formData)` / `submitAsync(formData)` also preserve multipart data when it contains a File or Blob; text-only imperative input keeps the JSON default. For a text-only multipart endpoint, pass `{ body: toFormData(values) }` directly or use a mapper that returns `{ body: formData }`. Unsafe field names are filtered without mutating the original FormData. `mapFormData` overrides these defaults, and server functions still receive FormData directly. Native file-upload forms should declare `encType="multipart/form-data"` so their pre-hydration fallback also transmits files. Use `mapFormData` when the validated input needs coercion or a different shape: ```tsx const quantity = useFetcher(apiClient.cart.post, { mapFormData(formData) { return { body: { productId: String(formData.get("productId")), quantity: Number(formData.get("quantity")), }, }; }, }); ``` After hydration, `` prevents navigation and submits through the typed target. For a server function, the function itself remains the native form action, preserving React's progressive-enhancement path before JavaScript loads. Generated GET and POST API forms use the real endpoint URL as their native fallback; a native fallback navigates to the endpoint response, while the hydrated fetcher stays on the page. If `mapFormData` throws, the target and optimistic callback are not called. The fetcher reports `status: "error"`, updates `error`, and runs `onError`/`onSettled` with `variables: undefined`. This also applies to `` and fire-and-forget `submit`; `submitAsync` rejects with the same error. `formData` is cleared when submission finishes. Older work still contributes to `pending`, but cannot overwrite this newer error; `reset()` clears it as usual. Local mapping failures are wrapped in `FetcherInputError` from `@farm.js/core/client`, with `code: "input_error"`, `status: 0` (no HTTP response), and the original thrown value in `cause`. Its message preserves an original Error message or thrown string. Fetcher error types and callbacks include this case alongside the target's existing errors. Narrow with `error instanceof FetcherInputError` before handling provider-specific errors. Request-level callbacks under `request` still receive only API-client errors, not local mapping failures. ## Client options - cache: choose cache-first, network-only, or stale-while-revalidate; `persist` allows the configured [client cache adapter](#persist-the-client-cache) to store the read. - retry: retry transient failures with a non-negative integer count and a finite delay from 0 through 2,147,483,647 milliseconds. - invalidate: mark typed route keys stale after mutations. - optimistic: update cached query data before the server response returns. - onRequest, onResponse, onSuccess, onError, onSettled, and onStatus: observe the full client lifecycle. Lifecycle callbacks are non-awaited observers. Farm reports synchronous throws and rejected promises without changing the request result, retry decision, optimistic update, or settlement. `onResponse` is a transport observer. If it throws or returns a rejected promise, Farm reports that failure through the platform `reportError` hook (or the console fallback) without retrying or changing the completed API result. Use a structured cache key when an API response intentionally shares data with route data or a [`createServerQuery`](/docs/server-queries): ```ts const { apiClient: publicApi } = createApiClients({ credentials: "omit" }); const product = await publicApi.products.get( { query: { id } }, { cache: { key: ["product", id], scope: "shared", policy: "stale-while-revalidate", staleTime: 30_000, }, }, ); ``` Structured keys use Farm's route-data key contract. Default API cache keys include the API origin. Same-origin and otherwise credentialed requests keep their cache private to the created client, and changing explicit headers or credentials clears that private cache. This prevents a new client from reusing a response produced under an earlier cookie identity without placing credential values in a public cache key. Invalidate session-specific reads when the same client logs in or out. Private caches follow the lifetime of their caller: on runtimes with `WeakRef`, the shared invalidation channel does not keep an abandoned cache alive. Cleanup occurs during finalization or a later invalidation, not on a guaranteed schedule. Keep reusable callers at module scope; `gcTime` controls expiration of individual entries, not caller disposal: an expired entry is evicted on its next read, and a periodic background sweep also removes expired entries that no consumer is watching, so unread keys do not accumulate in long-lived sessions. Older runtimes without `WeakRef` retain the existing strong subscription, so avoid repeatedly creating callers there. Request-local server caches do not subscribe to the global invalidation channel. Lazy expiry checks ownership only for the expired key and its aliases. Unrelated subscriptions and requests are not scanned on each expired read; aliases and invalidation metadata remain available while a related consumer or request still owns them. Explicit entry deletion follows the same metadata cleanup rules: unowned aliases and invalidation marks are released, while observed keys retain them until their last owner leaves. Deleting a key that has never held an entry does not discard an explicit invalidation made before its first write. Use `scope: "shared"` only for public data requested with `credentials: "omit"` and no custom headers that intentionally shares a structured key with route data or another API client: ```ts cache: { key: ["catalog", "featured"], scope: "shared", policy: "cache-first", } ``` Requests with `credentials: "omit"` and no custom headers may share by default because they carry no browser identity. `scope: "client"` can keep those requests private as well. Credentialed or header-carrying requests remain client-scoped even if `scope: "shared"` is supplied. Set `cache.dedupeMs` to join identical requests started within that window. If an older request is still running after the window expires, the newer request becomes the cache owner; the older result still returns to its original caller but cannot replace the newer cached value. The same ownership rule applies across different caller instances using a shared cache key. The newest network read owns that entry even if it fails or is cancelled; an older pending response cannot restore superseded data. Transport deduplication remains per caller, and private, credentialed, custom-transport, and request-local server caches remain isolated. Invalidation also protects against reads already in flight: their results still return to their callers, but cannot become fresh cache entries after that key is invalidated. The next read can fetch current data, even when invalidation and request startup happen in the same millisecond. Unrelated cache keys are unaffected. This includes mutation `invalidate` targets whose first read has not populated the cache yet. Explicit keys and typed route references receive the same protection in private, shared, and request-local server caches. Failed mutations leave those reads unchanged. ## Optimistic cache updates Farm's cache lifecycle is intentionally familiar to React Query and TanStack Query users, but it is implemented by Farm's own typed API client and shared cache. A mutation can update an existing query result immediately, roll it back after an error, and invalidate it after the server responds. ```ts const products = await apiClient.products.get( { query: { category } }, { cache: { key: ["products", category], policy: "stale-while-revalidate", staleTime: 30_000, }, }, ); const createProduct = apiClient.products.post( { body: { name, category, }, }, { optimistic: { update: [ [ products.key, (current) => ({ ...current, products: [{ id: "optimistic", name, category }, ...(current?.products ?? [])], }), ], ], rollbackOnError: true, }, invalidate: [products.key], }, ); await createProduct; ``` The updater runs synchronously before the POST finishes. `products.key` preserves the cached response type, so `current` is inferred from `apiClient.products.get`. You can also target a generated route directly with `[apiClient.products.get, { query: { category } }, updater]`. With `rollbackOnError: true`, Farm restores the exact previous cache entry when the mutation fails. After a successful mutation, invalidation marks the key stale so mounted consumers or the next read can load the canonical server result. Failed mutations do not invalidate known-good cached reads; when rollback is disabled, only cache entries changed optimistically are marked stale so the next read loads canonical data. Use `invalidate: { targets: [[apiClient.products.get, input]], refetch: true }` to refresh a previously cached API read in the background after a successful mutation. Farm reuses that read's resolved route, input, key, cache policy settings, retry settings, and deadline, forcing a fresh read instead of replaying the mutation. Duplicate targets refresh once. Explicit cache keys work too; expired/missing entries or keys populated outside the API client are only invalidated because they have no retained API read to replay. Background refetch resolves current header defaults, does not reuse the old caller's abort signal or per-call callbacks, and never repeats optimistic updates or mutation invalidations. Shared lifecycle hooks still observe it. A failed refetch leaves the entry stale and does not fail the already successful mutation. Read recipes expire or are removed with their cache entries. ## Cross-tab invalidation By default, invalidations stay inside the tab that produced them: a mutation in one tab marks keys stale there, while a second tab keeps its cached reads until its own focus or reconnect refresh fires. Opt into same-origin cross-tab propagation with `enableCrossTabCacheInvalidation`: ```ts "use client"; import { enableCrossTabCacheInvalidation } from "@farm.js/core/client"; // Call once during client startup; returns a disposer. enableCrossTabCacheInvalidation(); ``` Invalidations from mutations, server functions, and explicit cache invalidation are posted to a `BroadcastChannel` and applied in every other tab through the normal invalidation path, so mounted stale queries there refetch on their own. Only invalidation keys cross the channel, never cached data or responses, which leaves private and credentialed cache scoping untouched. Each tab refetches through its own credentials. The call is idempotent, safe during server rendering, and a no-op in environments without `BroadcastChannel`. Pass `channelName` to isolate multiple Farm apps served from one origin; enabling two different channel names in the same tab is an error. ## Live invalidation across clients Cross-tab propagation stays inside one browser. To make a write in one user's session invalidate mounted reads in other connected sessions, expose an app-owned server-sent event route. Authenticate the request before creating the stream, and filter every key to the tenant or user allowed to observe it: ```ts title="src/app/api/cache-events/route.ts" import { createFarmCacheInvalidationStream } from "@farm.js/core/cache"; import { requireSession } from "../../../auth/server"; export async function GET(request: Request) { const session = await requireSession(request); return createFarmCacheInvalidationStream({ signal: request.signal, filter: (key) => key.startsWith(`org:${session.orgId}:`), }); } ``` Connect once during browser startup: ```ts "use client"; import { enableLiveCacheInvalidation } from "@farm.js/core/client"; const dispose = enableLiveCacheInvalidation({ url: "/api/cache-events", }); ``` Farm batches invalidations emitted in the same microtask, sends only encoded cache keys, and applies them through the ordinary client invalidation path. Mounted stale queries therefore refetch using their existing inputs, cache policy, and current browser credentials. Native `EventSource` reconnection handles transient disconnects; Farm's existing focus and reconnect revalidation covers events missed while the browser was offline. The stream is best-effort, not a durable event log: reconnects do not replay missed keys. To bound memory for slow clients, Farm limits queued encoded output to 64 KiB and each pending microtask batch to 1,024 distinct keys / 64 KiB of UTF-16 key data. Overflow (including an oversized key or encoded batch) errors the stream and releases its listener and heartbeat; EventSource can reconnect normally. Heartbeats are skipped while output is queued. Apps requiring guaranteed delivery should use a durable event source and explicitly resynchronize reads after reconnecting. The stream forwards events from the current server process. A multi-instance deployment needs a shared pub/sub backplane feeding each instance's invalidation bus. That is intentionally outside the first version of this API. The selected deployment target must also support a long-lived streaming response. Do not expose a broad stream or put sensitive tenant data in unscoped cache keys: event payloads contain keys, not cached values, but key names can still reveal information. `enableLiveCacheInvalidation` shares one `EventSource` for repeated calls with the same URL and credentials and closes it after the final disposer. Live events are not echoed through the optional cross-tab bridge, because every connected tab receives the server event directly. Set `withCredentials: true` only for a cross-origin endpoint that accepts credentialed EventSource requests. ## Persist the client cache The browser cache is in memory: a reload starts cold. Opt into persistence by pointing `cache.client.adapter` at a client module and marking the reads that may be stored: ```ts title="farm.config.ts" export default defineConfig({ cache: { client: { adapter: "./src/cache-adapter", }, }, }); ``` The path resolves from the project root and the module is bundled into the browser entry — the server never imports it. Its default export implements five callbacks over any storage: ```ts title="src/cache-adapter.ts" import { defineClientCacheAdapter, type PersistedEntry } from "@farm.js/core/client"; const PREFIX = "farm-cache:"; export default defineClientCacheAdapter({ async keys() { return Object.keys(localStorage) .filter((key) => key.startsWith(PREFIX)) .map((key) => key.slice(PREFIX.length)); }, async get(key) { const raw = localStorage.getItem(PREFIX + key); return raw ? (JSON.parse(raw) as PersistedEntry) : null; }, async set(key, entry) { localStorage.setItem(PREFIX + key, JSON.stringify(entry)); }, async delete(key) { localStorage.removeItem(PREFIX + key); }, async clear() { for (const key of Object.keys(localStorage)) { if (key.startsWith(PREFIX)) localStorage.removeItem(key); } }, }); ``` Farm owns every policy decision — what may be stored, when writes flush, how entries load — and the adapter is plain keyed storage. `set` is an upsert; there is no separate create. An adapter over IndexedDB, OPFS, or a native bridge has the same shape, and the optional `setMany`/`getMany` batch methods let it use one transaction per flush. `storageClientCacheAdapter` wraps any Farm/unstorage-style key-value client into this contract, mirroring the server's `storageCacheAdapter`, so one KV implementation can serve both caches — against two physically separate stores. ### Nothing persists without opting in Persisted data lives on the device, so persistence is allowlist-only. Mark reads where they are declared: ```ts export const catalogQuery = createServerQuery({ key: () => ["catalog", "featured"], staleTime: "5m", persist: true, async handler() { return loadCatalog(); }, }); ``` ```ts await apiClient.products.get( { query: { category } }, { cache: { key: ["products", category], policy: "stale-while-revalidate", staleTime: 30_000, persist: true, }, }, ); ``` Only successful, `persist`-flagged entries in the shared cache are written, debounced in the background. Private and credentialed API caches never reach the adapter regardless of the flag. `cache.client` also accepts `version` — a build or deploy id salted into every entry so a deploy drops incompatible data — and an optional coarse `persistKey` filter on the engine. ### Lifecycle and limits On startup, persisted entries load **stale-but-visible**: the data renders immediately and the normal stale-while-revalidate path refreshes it on first read, so freshness semantics do not change. Expired and version-mismatched entries are dropped during load. Deletes, garbage collection, and `clear()` mirror to the adapter. If the adapter fails — quota, private browsing, a broken driver — persistence disables itself for the session and the app continues memory-only; the cache is disposable by design. Call `clearPersistedCache()` on logout or any session change so the next visitor on a shared device cannot warm-start into another user's data: ```ts import { clearPersistedCache } from "@farm.js/core/client"; await clearPersistedCache(); ``` Persistence stores confirmed reads only. Pending optimistic updates and in-flight mutations are never written; a reload drops them. ## Result shape App-route and integration callers both expose `data` and `error`, so callers can branch on `result.error`. App-route results also include a typed cache `key`; integration results retain their own error contract and do not include that cache key. ```ts const result = await apiClient.hello.post({ body: { name: "Ada", }, }); if (result.error) { console.error(result.error.status); return; } console.log(result.data.message); ``` This makes client components easier to write because failed responses do not need to be caught with `try/catch` unless you want that behavior. For app routes, if an HTTP error body is malformed, Farm still returns an `http_error` with the real status and `Response`; the decoding failure is available as `error.cause`. ## Server callers Import `api` from the same shared module. No endpoint imports, second factory, or request-bound instance are needed: ```tsx import { api } from "@/lib/api"; export default async function Page() { const result = await api.hello.post({ body: { name: "Ada" } }); if (result.error) throw result.error; return

{result.data?.message}

; } ``` `api` resolves the current request and that app's registered routes at call time. It works in Farm server pages, queries, actions, and API handlers in development and the default universal production runtime. Constructing the pair at module scope is safe. Calling `api` in the browser or outside an active Farm request throws an actionable error; it never silently falls back to HTTP. The local caller uses the app's server API mount, including custom base paths. A public `baseURL` pointing at another origin affects `apiClient`, not local dispatch. The current request's `Cookie`, `Authorization`, and `Accept-Language` headers are inherited; shared options and per-call headers can override them. Other headers are not implicitly forwarded. Do not place private tokens in the shared module: read them in server code and pass them per call where needed. Local calls use the same route matching, params, input/output validation, endpoint middleware, body limits, and response decoding as HTTP routes. They still serialize request/response data; they avoid a network round trip, not all serialization. Request cancellation is inherited. Opt-in caches and in-flight requests are isolated by request and credentials, never shared between users. Dynamic `$params()` scopes work identically for both callers. ### Direct-call boundaries `api` is a direct endpoint caller, **not a replay of the full HTTP request pipeline**. It does not run path-level HTTP middleware, redirects/rewrites, plugin request/response lifecycle hooks, or deployment-layer checks. Put authorization and other rules required for both transports in the endpoint's `middleware`. Use `apiClient` when an operation must go through HTTP middleware. Response cookies and headers from a local endpoint do not automatically become headers on the outer page response. Post-response work scheduled by an endpoint uses the enclosing request's lifecycle. Integrations remain available under `api.integrations` and `apiClient.integrations`, using their existing integration-dispatch semantics. ### Existing factories `createAPIClient()` remains supported for an HTTP-only caller, including standalone scripts with an absolute `baseURL`. `createServerAPIClient({ hello: { get: GET } })` remains supported for explicit endpoint-function maps in server-only modules. It returns the supplied functions and their raw results; passing only `{ request }` does **not** discover routes. Prefer the paired factory for a shared module and a consistent `{ data, error, key }` app-route result. The HTTP-only factory retains the same route and integration inference: ```ts import { createAPIClient } from "@farm.js/core/client"; import type { APIRouter } from "./api.generated"; import type { AppIntegrations } from "./integrations"; const http = createAPIClient(); // Integration callers are under http.integrations.. const routesOnly = createAPIClient({ integrations: false }); // Only the app's route tree is exposed; no reserved integration caller namespace. ``` `integrations: false` disables only the caller namespace, not the configured providers or their HTTP routes. These overloads are available from the published `@farm.js/core/client` entry; there is no need to import framework internals or cast the options. ## Integration callers Use the same shared module for file routes, plugin routes, and configured integrations: **src/lib/api.ts** ```ts import { createApiClients } from "@farm.js/core/client"; import { apiRoutes, type APIRouter } from "./api.generated"; import type { AppIntegrations } from "./integrations"; export const { api, apiClient } = createApiClients({ routes: apiRoutes, }); ``` Export `AppIntegrations = typeof appIntegrations` from the server-only module containing the registry passed to `farm.config.ts`'s `integrations` field. Import only its type here, not the registry value or provider SDKs. The type describes existing integrations; it does not register them. Farm supplies their caller metadata at runtime. File and plugin routes keep their generated paths, such as `apiClient.hello.post(...)`. Integrations live under the reserved `.integrations` namespace on both callers. For example, with the `billing` integration from the [custom integration guide](/docs/integrations/custom#shared-registration): ```ts // Browser code const checkout = await apiClient.integrations.billing.checkout.post({ body: { priceId: "price_123" }, }); // Server code const serverCheckout = await api.integrations.billing.checkout.post({ body: { priceId: "price_123" }, }); ``` Integration-specific defaults belong in `integrations: { data, headers, ... }` in the shared factory options. Only put browser-safe values there. Set `integrations: false` if the app does not need the reserved namespace. The shared factory does not change integration execution semantics. Integration calls return `{ data, error }`, not the app-route `{ data, error, key }` cache contract. Server integration calls dispatch to a registered handler when possible and can fall back to HTTP when local dispatch is unavailable. App-route `api` calls instead require an active Farm server request and never fall back to HTTP. Operations marked `isServer: true` remain available only through `api.integrations`, not `apiClient.integrations`. ### Integration-only callers `createIntegrations()` remains supported and is not deprecated. Existing apps do not need to migrate. Use it when only integration callers are needed, or when you prefer to keep them separate from app-route callers. It returns integration namespaces directly: `apiClient.billing.checkout.post(...)` and `api.billing.checkout.post(...)`. With `createApiClients`, those same calls need the `.integrations` segment. Choose one setup for the shared module; do not create both pairs for the same integrations. Switching factories requires updating the namespace and moving shared integration defaults into the `integrations` option; it is not a drop-in rename. The paired factory discovers configured integrations; it does not accept the integration-only factory's explicit source map or separate server-options argument. Options are another reason to choose the separate factory. Shared `baseURL`, `headers`, `credentials`, and `data` defaults work with either setup: put them under `integrations` when using `createApiClients()`. Keep `createIntegrations()` when you want setup-level `request` or `forwardHeaders`, separate server defaults, or explicit source maps. The paired factory's server integration calls still accept per-call overrides. See the [options comparison and request-scoped example](/docs/integrations#integration-only-setup). Keep request-bound callers and private server options in server-only code, not in the shared browser module. For deliberately separate modules, use `createApiClients({ routes: apiRoutes, integrations: false })` for app routes and `createIntegrations()` for integration callers. Disabling the paired factory's namespace does not unregister integrations or their HTTP routes; it only leaves integration access to the separate caller module. ## Server Function Form Actions `createServerFn` pairs with `useServerFn` when a mutation is naturally a form action. Use `optimistic` to show the next UI state immediately, then let the server result replace it when the action completes. Both hooks accept `retry` with the API client's shape, `{ count, delay }`, where `delay` is a fixed wait or an `(attempt) => ms` backoff starting at attempt `1`. The default remains a single attempt. Retries rerun the whole submission against the server function, so keep retried handlers idempotent. `reset()` stops a waiting retry: the submission rejects with its last error and no further attempts start. For both `useServerFn` and `useAction`, `reset()` restores `initialResult` (or `null`), clears the error, and returns the hook to idle. It does not cancel server work: promises from earlier submissions still resolve or reject, but their completions no longer change hook state or invoke completion callbacks. Requests started after the reset have their own pending count, so older work cannot clear their pending state. The `onSuccess`, `onError`, and `onSettled` options on these hooks are notification callbacks. Thrown errors and rejected promises from them are reported through `reportError`, or the console when unavailable, without replacing the server result/error or changing the pending count. Callbacks are not awaited; put required work in the server function or explicitly await it after the call. If `onSuccess` or `onError` synchronously resets the hook or starts another submission, the superseded submission's `onSettled` callback is skipped. `throwOnFormError` still controls whether `formAction` rethrows the server error, not a notification error. **src/actions/todos.ts** ```ts import { createServerFn } from "@farm.js/core/server-fn"; import { z } from "zod"; export const addTodo = createServerFn({ input: z.object({ title: z.string().min(1), }), output: z.object({ todos: z.array( z.object({ id: z.string(), title: z.string(), }), ), }), async handler({ input, signal }) { signal.throwIfAborted(); return { todos: await db.todo.create({ data: input }), }; }, }); ``` `input` validates values before the handler runs. An optional `output` schema validates the resolved handler result before it crosses the server-function boundary. Its parsed type becomes the function's return type, and schema transforms are supported: ```ts const PublicUser = z.object({ id: z.string(), email: z.string().email(), }); export const getUser = createServerFn({ input: z.object({ id: z.string() }), output: PublicUser, async handler({ input }) { // PublicUser strips passwordHash before this result can reach the browser. return db.user.findUniqueOrThrow({ where: { id: input.id } }); }, }); ``` Output parsing also runs for direct server calls, form actions, and browser calls. Invalid results reject the function just like invalid input. Keep the output contract narrow for private data; do not rely on TypeScript alone to prevent an extra database field from being returned at runtime. ### Composable middleware Use `createServerMiddleware` for server-only behavior shared by several functions, such as session loading, authorization, transactions, rate limits, and auditing. Middleware can depend on other middleware, and every context value is inferred by functions that install it. ```ts import { createServerFn, createServerMiddleware } from "@farm.js/core/server-fn"; const withSession = createServerMiddleware({ async handler({ request, next }) { if (!request) throw new Error("A request is required"); const session = await getSession(request); if (!session.user) throw new UnauthorizedError(); return next({ context: { session } }); }, }); const withTransaction = createServerMiddleware({ middleware: [withSession], async handler({ context, next }) { return db.transaction((tx) => next({ context: { tx } })); }, }); export const renameProject = createServerFn({ middleware: [withTransaction], input: z.object({ projectId: z.string(), name: z.string().min(1) }), async handler({ input, context }) { // context.session and context.tx are both typed. await requireProjectEditor(context.session, input.projectId); return context.tx.project.update({ where: { id: input.projectId }, data: { name: input.name }, }); }, }); ``` Dependencies run first and are de-duplicated by middleware identity. For `middleware: [withTransaction, withAudit]`, a shared `withSession` dependency runs once. The chain uses onion ordering: code before `await next()` runs from outer to inner, and code after it unwinds from inner to outer. Every middleware must call `next()` exactly once and return its result. Throw to reject a request; middleware cannot silently skip the handler. Input validation finishes before the chain starts, while output validation runs after the whole chain unwinds. Context is created on the server, shallowly frozen, and never accepted from the browser. Keep shared authentication in middleware, but still perform resource-specific authorization where the resource is loaded. Derive identities, roles, tenant IDs, and rate-limit keys from the trusted request or server state, never from unvalidated client fields. Middleware errors use the same sanitized server-action error boundary as handler errors. **src/components/todo-form.tsx** ```tsx "use client"; import { useServerFn } from "@farm.js/core/server-fn/client"; import { addTodo } from "../actions/todos"; export function TodoForm() { const action = useServerFn(addTodo, { initialResult: { todos: [] }, rollbackOnError: true, optimistic({ current, formData }) { return { todos: [ ...(current?.todos ?? []), { id: "draft", title: String(formData?.get("title") ?? "") }, ], }; }, }); return (
); } ``` The optimistic callback receives the raw input, `formData` for form submissions, and the current result. Return `undefined` when a submission should not change the optimistic result. Use `rollbackOnError` for reversible UI state; keep authorization and validation on the server function itself. For `useServerFn` and `useAction`, `current` includes earlier optimistic transitions from the same React batch. Three submissions that each return `(current ?? 0) + 1` from an initial result of `0` therefore show `3`, even before React rerenders. The callback runs once per submission, including in Strict Mode. A failed latest submission with `rollbackOnError` restores the result immediately before that submission, including a previous optimistic value; a same-batch `reset()` restores the initial snapshot for subsequent submissions. If an optimistic callback itself resets or starts new work, the older transition cannot replace the reset or newer result. Reset still does not cancel execution of an already-started submission. When the function is called from the browser, `request` is the underlying Web `Request` and `signal` aborts with that request. A direct call made while rendering can inherit the current render request; a background or direct call outside request scope has no `request` and receives a stable, non-aborted signal. The same values are available to middleware. Pass `signal` to database or network clients that support cancellation. Farm validates action origin metadata, accepted form/RSC content types, action ID shape, and request size before decoding an action. Browser calls use same-origin credentials and refuse redirects. Unexpected thrown values are logged on the server but become a generic `ServerActionError` in the browser, so secrets and stack traces are not serialized. ### Typed server function errors Declare expected failures next to the input contract. `error` accepts only declared codes, validates the public payload, and preserves the code, status, and data across the RSC action transport: ```ts export const updateProduct = createServerFn({ input: updateProductSchema, errors: { NOT_FOUND: { status: 404, data: z.object({ id: z.string() }), }, }, handler({ input, error }) { const product = findProduct(input.id); if (!product) { return error("NOT_FOUND", { id: input.id }); } return product; }, }); ``` `useServerFn`, `useMutation`, and `useFetcher` infer the declared error union. Narrow by `name` and `code` to recover the exact payload: ```tsx const update = useServerFn(updateProduct); if (update.error?.name === "ServerFnFailure" && update.error.code === "NOT_FOUND") { // id is inferred as string. showMissingProduct(update.error.data.id); } ``` Add an optional `message` only when it is safe to display publicly. Declared error data schemas must support synchronous `parse()` or `safeParse()` because `error()` throws immediately. Hydrated calls carry `status` inside the Flight error envelope; progressive form submissions also use it as the HTTP status. Unexpected exceptions remain sanitized as a generic `ServerActionError`, without their message, stack, or custom properties. Action references identify which function to execute; they are not authorization tokens. Check authentication, roles, tenant ownership, and resource access inside every action that reads or changes private data. ### Call server functions from agents Add `mcp: { name: "..." }` to a server function or query and list it in `mcp.tools` to make it an [MCP tool](/docs/plugins/mcp#server-functions-and-queries). Agents then call the same function as your UI, through its own schema and middleware, without the browser action transport. ## Production notes - Keep generated API types committed or generated during CI. - Prefer typed body/query schemas for mutations. - Use server callers for secrets, auth cookies, and internal-only provider actions. - Use invalidation after mutations that change cached route data. - Keep optimistic updates scoped to UI state you can confidently roll back. - Keep `serverActions.allowedOrigins` narrow and use API routes for intentionally cross-origin callers. - Return typed expected failures; reserve thrown errors for unexpected failures. - Add narrow output schemas to functions that return private database records. --- ## API Routes URL: /docs/api-routes Expose HTTP handlers from src/app/api and validate input with schemas before handler code runs. # API Routes Expose HTTP handlers from src/app/api and validate input with schemas before handler code runs. ## Route handlers API route modules export HTTP methods. Farm discovers them, runs the route pipeline, and can generate typed client callers from the route shape. Routes in one app source must have distinct URL shapes. For example, `/api/users/[id]` and `/api/users/[slug]` conflict even though their parameter names differ. Parameters must have unique, non-reserved names, and catch-alls must be the final segment. These checks also run in RSC builds. If a project overrides a layer's equivalent dynamic shape, the project route replaces it completely, so handlers do not receive parameters named by the lower-priority route. **src/app/api/hello/route.ts** ```ts import { createEndpoint } from "@farm.js/core/api"; import { z } from "zod"; export const POST = createEndpoint( { body: z.object({ name: z.string().min(1), }), }, async ({ body }) => { return Response.json({ message: "Hello " + body.name }); }, ); ``` The export name sets the HTTP method, so route files don't pass `method`. An endpoint without schemas or other options can take the handler alone: **src/app/api/stars/route.ts** ```ts import { createEndpoint } from "@farm.js/core/api"; export const GET = createEndpoint(async () => Response.json(await getStars(), { headers: { "cache-control": "public, s-maxage=600, stale-while-revalidate=3600" }, }), ); ``` `method` only matters for endpoints created with an explicit path, such as `createEndpoint("/api/settings", { method: "POST" }, handler)`, which have no export name to read it from. There it defaults to `GET`. Use [`createApiClients()`](/docs/api-client) in one shared `src/lib/api.ts` to return `{ api, apiClient }` from the generated router. Both callers reuse this route definition: `api` dispatches locally on the server and `apiClient` sends HTTP requests. Endpoint middleware runs for both; outer HTTP middleware is not replayed by direct calls. Never import endpoint modules into the shared caller file. ## MCP tool opt-in When `@farm.js/mcp` is installed, a typed endpoint can opt into the authenticated MCP transport with `mcp: true` or explicit tool metadata: ```ts export const GET = createEndpoint( { mcp: { name: "list_projects", description: "List projects visible to the current user.", readOnlyHint: true, }, }, async () => ({ projects: await listProjects() }), ); ``` The route still owns validation and endpoint middleware. The top-level `mcp` config separately owns transport authentication in `farm.config.ts`. Alternatively, select explicitly pathed endpoint instances in `mcp.tools` and return allowed tool names from `mcp.authorize` to control discovery and invocation per caller. The same list can include standalone `defineTool()` definitions from `@farm.js/mcp` for operations without an HTTP route. See the [API MCP guide](/docs/plugins/mcp) for mixed composition, generated names, and security boundaries. ## Next-style exports You can also manually export GET, POST, PATCH, and other handlers from the route file. Farm keeps this familiar while layering typed helpers around it. Generated API client types follow runtime value exports. Comments, string examples, and type-only exports named after HTTP methods do not create callable endpoints. **src/app/api/status/route.ts** ```ts export async function GET() { return Response.json({ ok: true }); } ``` ## Validation > **Zod and standard schema** > > Endpoint and integration route inputs can use Zod or compatible standard-schema validators so the handler sees parsed input instead of raw unknown data. ## Route file shape Farm follows the familiar route-file convention: each route lives in `src/app/api/**/route.ts` and exports one or more HTTP methods. The equivalent `route.tsx`, `route.js`, and `route.jsx` filenames use the same discovery and generated-type behavior. ```txt src/app/api/hello/route.ts -> /api/hello src/app/api/users/[id]/route.ts -> /api/users/:id src/app/api/files/[...path]/route.ts -> /api/files/* ``` Static route directory names may contain Unicode. Browsers percent-encode those path segments; Farm decodes both the discovered route and request segment before matching them. Defining the same method and path twice within one app source is an error, including in production RSC builds. The error names both files. Project routes may still override matching methods from a layer; other methods on the layer route remain available. Use `createEndpoint` when you want input validation and typed client generation. Use plain `GET`, `POST`, `PATCH`, and friends when you want to handle the raw `Request`. Plain handlers always receive that `Request`; parameter names such as `request`, `context`, or `ctx` do not change the calling convention. Use `createEndpoint` for the parsed `{ body, query, headers }` context. `HEAD` follows normal HTTP semantics. A route can export a dedicated `HEAD` handler, otherwise Farm uses its `GET` handler and returns the same status and headers without a response body. This also applies to production RSC builds. They support `QUERY` exports, prefer an explicit `HEAD` handler, and return `405` with an `Allow` header for unsupported methods. ## Body and query input ```ts export const GET = createEndpoint( { query: z.object({ q: z.string().optional(), page: z.coerce.number().int().positive().default(1), }), }, async ({ query }) => { return Response.json({ q: query.q ?? "", page: query.page, }); }, ); ``` Farm parses and validates `body`, `query`, and `headers` before middleware or handler code runs. Header schema keys use the lower-case names exposed by the Fetch `Headers` API. Invalid input returns a `400` response with structured validation issues. Generated callers use the schema's **input** type for body and query values; middleware and handlers receive its parsed **output** type. For example, a field declared as `z.string().transform(Number)` is sent as a string and received by the handler as a number. Defaulted fields may be omitted by the caller. The same distinction applies to multipart `toFormData(...)` inputs, Standard Schema input/output types, and both `api` and `apiClient` from `createApiClients()`. Validation still runs on the server; this does not send schemas or transforms to the browser. Existing endpoints without transforms retain their input types. Malformed `application/json` and `application/*+json` bodies return `400` before endpoint middleware or handler code executes, including when the endpoint does not declare a body schema. Empty declared JSON receives the same response for non-QUERY methods. A bodyless `QUERY` remains valid because QUERY requires `Content-Type` and generated clients attach `application/json` even when the endpoint declares no body. Malformed `multipart/form-data` also returns `400` instead of becoming an absent body; an empty `application/x-www-form-urlencoded` body is a valid empty form and parses as an empty object. Callers that omit `Content-Type` retain the permissive JSON parsing path for compatibility. If an upload is aborted while Farm is buffering its body, Farm rejects it before invoking the endpoint. This does not roll back work in a handler that has already started. On Node, finishing an upload is not an abort: only an interrupted upload or an early response disconnect cancels the request signal. ## HTTP QUERY Use the standardized [`QUERY` HTTP method](https://www.rfc-editor.org/rfc/rfc10008.html) when a read operation needs structured request content that is too large or sensitive for a URL. Like `GET`, `QUERY` is safe and idempotent; unlike `GET`, its request body has defined semantics. **src/app/api/products/search/route.ts** ```ts import { QUERY as createQueryEndpoint } from "@farm.js/core/api"; import { z } from "zod"; export const QUERY = createQueryEndpoint( { body: z.object({ filters: z.array(z.object({ field: z.string(), value: z.string() })), limit: z.number().int().min(1).max(100).default(20), }), }, async ({ body }) => { const products = await searchProducts(body.filters, body.limit); return { products, total: products.length }; }, ); ``` The helper infers the validated `body` inside the handler and exposes the same input and response types to the generated client. QUERY requests must include a `Content-Type` header; Farm's generated client sets `application/json` automatically. A raw handler is also valid: ```ts export async function QUERY(request: Request) { const search = await request.json(); return Response.json(await searchProducts(search)); } ``` Keep `QUERY` handlers read-only. Use `POST`, `PATCH`, or another unsafe method when the operation changes server state. A server can advertise accepted query media types with an `Accept-Query` response header. Cross-origin browser requests use a CORS preflight, so include `QUERY` in the configured `cors.methods` list when that list is restricted. ## Uploads and streaming results Use `multipart()` when an endpoint accepts files. Farm parses the request as `FormData`, preserves `Blob`/`File` values and repeated fields, then runs the same body-schema validation used for JSON: ```ts import { createEndpoint, jsonStream, multipart } from "@farm.js/core/api"; import { z } from "zod"; const importBody = multipart( z.object({ title: z.string().min(1), file: z.custom((value) => value instanceof Blob), }), ); type ImportEvent = { phase: "accepted"; bytes: number } | { phase: "complete"; imported: number }; export const POST = createEndpoint( { body: importBody, }, async ({ body }) => { async function* importEvents(): AsyncGenerator { yield { phase: "accepted", bytes: body.file.size }; const imported = await importRows(body.file); yield { phase: "complete", imported }; } return jsonStream(importEvents()); }, ); ``` `jsonStream()` uses newline-delimited JSON (`application/x-ndjson`). It sends each typed event as soon as the source yields it, respects response backpressure, cancels the source when the reader disconnects, and defaults to `Cache-Control: no-store`. Use ordinary `Response` objects for binary downloads or protocols that are not JSON event streams. The client decoder rejects malformed NDJSON with its original `SyntaxError` and cancels the source without waiting for cleanup or an unread response clone. Explicit stream cancellation and early iterator return still await the producer's cleanup. When Farm bridges a streamed `Response` to Node, a failed response write also cancels the upstream body. Put producer cleanup in the stream's `cancel()` callback. Farm preserves the original write error and does not wait indefinitely for that cleanup to finish. Disconnects follow the same rule, including when the connection closes under backpressure: Farm releases its reader and response listeners without waiting for app-owned cancellation work. ## Endpoint middleware Put plain async functions in `middleware`. There is no middleware factory and no `next()` callback. Functions run in declaration order after endpoint input validation. **src/app/api/projects/[id]/route.ts** ```ts import { createEndpoint, type EndpointMiddlewareContext } from "@farm.js/core/api"; import { z } from "zod"; type Session = { user: { id: string; roles: string[] }; }; async function requireAuth({ request }: EndpointMiddlewareContext) { const session = await getSession(request); if (!session?.user) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } return { session: session as Session }; } const requireRole = (role: string) => async ({ context }: EndpointMiddlewareContext<{ session: Session }>) => { return context.session.user.roles.includes(role); }; async function loadProject({ body, context, params, }: EndpointMiddlewareContext<{ session: Session }, { name: string }>) { const project = await db.project.findUniqueOrThrow({ where: { id: String(params.id), ownerId: context.session.user.id, }, }); return { project }; } export const PATCH = createEndpoint( { body: z.object({ name: z.string().min(1) }), middleware: [requireAuth, requireRole("admin"), loadProject], }, async ({ body, context }) => { // context.session and context.project are inferred from middleware returns. return db.project.update({ where: { id: context.project.id }, data: { name: body.name }, }); }, ); ``` Middleware return values have deliberate control-flow meaning: | Return value | Result | | ------------ | ---------------------------------------------------------------------------------------- | | Plain object | Shallow-merges its properties into typed `context` for later middleware and the handler. | | `true` | Continues without adding context. | | `false` | Stops and returns Farm's JSON `403 Forbidden` response. | | `Response` | Stops and returns that response unchanged. Use this for custom status, body, or headers. | Only literal `false` is the default denial signal. Returning `null`, `undefined`, an array, or another value is an error, which catches forgotten returns instead of silently skipping authorization. Context is shallowly frozen, duplicate keys are rejected, and `__proto__`, `constructor`, and `prototype` cannot be provided as context keys. Endpoint middleware is local to one `createEndpoint` declaration and can use its validated input. Use `src/app/**/middleware.ts` for path-level behavior shared by many routes, such as request tracing, common headers, or an early rewrite before endpoint parsing. > **Authorization boundary** > > Derive users, roles, tenants, and rate-limit identities from the trusted `Request` or server state. Middleware context is created only on the server and is never accepted from client input. Keep resource-specific permission checks close to the resource query even when shared authentication runs in middleware. ## Typed expected errors Declare failures that are part of an endpoint's public contract, then return them through the typed `error` function: ```ts export const POST = createEndpoint( { body: z.object({ name: z.string().min(1), }), errors: { duplicate: { status: 409, message: "A product with this name already exists", data: z.object({ existingId: z.string(), }), }, forbidden: { status: 403, data: z.object({ permission: z.string(), }), }, }, }, async ({ body, error }) => { const existing = await findProductByName(body.name); if (existing) { return error("duplicate", { existingId: existing.id, }); } return createProduct(body); }, ); ``` Error codes and payloads are checked in the handler and carried into the generated API client: ```ts const result = await api.products.post({ body: { name }, }); if (result.error?.code === "duplicate") { // existingId is inferred as string. showExistingProduct(result.error.data.existingId); } ``` Farm validates the failure payload before returning a JSON error response. Declared messages and payloads are public, so do not include secrets. Undeclared exceptions remain unexpected server errors and are not converted into a declared failure. Endpoints without an `errors` declaration keep the existing `Error` client type. The generated client makes this endpoint RPC-like to call, but the transport remains a regular HTTP request and JSON error response. Existing endpoint definitions using `schema` and `fail` continue to work as deprecated aliases; use `data` and `error` in new code for consistency with server functions. ## Client inference Routes become client namespaces from their path: ```ts await api.hello.post({ body: { name: "Ada", }, }); await api.users.get({ query: { limit: 10, }, }); ``` The exact generated shape comes from route generation. Body and query schemas become typed caller input, and path segments become the nested `api.users.get` style namespace. During `farm dev`, Farm regenerates route/API types when page or API route files are added, changed, or removed. Run `farm generate` when you want the same refresh outside the dev server. ## Declare invalidation with the mutation When every caller of a mutation makes the same data stale, declare that relationship on the endpoint instead of repeating client-side invalidation: ```ts export const PATCH = createEndpoint( { body: z.object({ id: z.string(), name: z.string().min(1), }), invalidates: ({ body }) => [ { key: ["product", body.id] }, { key: ["products", "list"] }, { tag: "products" }, { path: "/products" }, ], }, async ({ body }) => { return db.product.update({ where: { id: body.id }, data: { name: body.name }, }); }, ); ``` Farm applies declared keys, tags, and paths to the server cache after the handler succeeds. Normal `api.products.patch(...)` callers also receive the key invalidations through response metadata, so matching browser queries become stale without repeating an `invalidate` option. A response with a status of 400 or higher, or middleware that stops before the handler, does not invalidate. The resolver receives validated body, query, and headers plus the accumulated typed middleware context. Invalidation is declarative rather than inferred: database writes do not reliably reveal every affected query. Existing client-side `invalidate` options remain supported for caller-specific cache relationships, and handlers can continue calling `invalidate(...)` or `revalidatePath(...)` directly. ## When to use integrations instead Use API routes for app-owned endpoints. Use integrations when a provider or feature needs a package-like surface: config validation, lifecycle hooks, database schemas, middleware, providers, and typed callers bundled together. --- ## Built-in Authentication URL: /docs/auth Enable Farm's built-in email/password authentication with one config key, server helpers, and a React hook. # Built-in Authentication Farm Auth is a built-in Farm framework feature for applications that need ordinary email/password authentication without configuring a provider instance. It is enabled through the top-level `auth` framework config—not through `integrations.auth`. The implementation lives in the first-party `@farm.js/auth` runtime package so database drivers and the authentication engine do not bloat `@farm.js/core`. Farm loads that package only when built-in auth is enabled. Install the optional runtime: ```bash pnpm add @farm.js/auth ``` Then enable the built-in feature: ```ts import { defineConfig } from "@farm.js/core"; export default defineConfig({ auth: true, }); ``` That one key: - enables email/password sign-up and sign-in; - mounts `GET` and `POST` auth actions below `/api/auth`; - creates and refreshes database-backed sessions; - exposes Farm server helpers and React APIs; - uses `.farm/auth.sqlite` in local development; - uses `DATABASE_URL` in production. `auth: { enabled: true }` is also accepted when keeping feature switches in object form. `auth: true` is the canonical shorthand. ## Start from a complete app The [Farm.js Auth Starter](https://github.com/farming-labs/farmjs-auth-starter) demonstrates the complete built-in path: email/password forms, server helpers, the React hook, protected middleware, local SQLite, and production Postgres configuration. ```bash git clone https://github.com/farming-labs/farmjs-auth-starter.git cd farmjs-auth-starter pnpm install pnpm dev ``` ## Server helpers Read the current session or user from a Server Component, server function, middleware, or route handler: ```ts import { auth } from "@farm.js/auth/server"; const session = await auth.session(); const user = await auth.user(); ``` Both return `null` for an anonymous request. Use the same methods with `required: true` when the request must be authenticated: ```ts const user = await auth.user({ required: true }); ``` An anonymous request throws a `401` response with the stable code `FARM_AUTH_REQUIRED`. There is no separate `requireUser` API to learn. Farm Auth does not configure or inspect pages. Authentication answers who made the current request; each page, server function, or API route decides what that user may do. ## React hook Use `useAuth` in a Client Component: ```tsx "use client"; import { useAuth } from "@farm.js/auth/client"; export function AccountMenu() { const { user, isPending, signOut } = useAuth(); if (isPending) return Loading…; if (!user) return Sign in; return ; } ``` The client entry also exports `signIn`, `signUp`, `signOut`, and `getSession` for forms that do not need the hook: ```ts import { signIn, signUp } from "@farm.js/auth/client"; await signUp({ name: "Ada Lovelace", email: "ada@example.com", password: "correct-horse-battery-staple", }); await signIn({ email: "ada@example.com", password: "correct-horse-battery-staple", }); ``` ## Configuration The default is intentionally useful. Add options only when application policy differs: ```ts export default defineConfig({ auth: { enabled: true, appName: "Acme", emailAndPassword: { requireEmailVerification: true, minPasswordLength: 12, }, session: { expiresIn: 60 * 60 * 24 * 30, updateAge: 60 * 60 * 24, }, }, }); ``` | Option | Default | Purpose | | ------------------------------- | ------------------- | ------------------------------------------------- | | `enabled` | `true` | Temporarily disable configured auth. | | `appName` | `"Farm app"` | Name used in auth metadata and messages. | | `basePath` | `"/api/auth"` | Route prefix for auth actions. | | `emailAndPassword` | enabled | Email verification and password length policy. | | `session` | 7 days | Session lifetime and refresh interval in seconds. | | `database.url` | `DATABASE_URL` | Explicit Postgres connection string. | | `database.path` | `.farm/auth.sqlite` | Local SQLite path. | | `database.migrateInDevelopment` | `true` | Update the local schema lazily on first auth use. | This interface stays at application-policy level. It does not expose a Better Auth instance or require Better Auth imports in application code. `basePath` accepts a stable application pathname, with or without its leading slash. URLs, query strings, fragments, dot segments, backslashes, control characters, and encoded path separators are rejected. If `basePath` is customized, create matching client helpers once: ```ts import { createFarmAuthClient } from "@farm.js/auth/client"; export const { useAuth, signIn, signUp, signOut } = createFarmAuthClient({ basePath: "/account/auth", }); ``` ## Database and deployment Local schema creation happens lazily when auth is first used. Loading `farm.config.ts` and running `farm build` never connect to the database and never run migrations. For production, set: ```bash DATABASE_URL=postgres://... FARM_AUTH_SECRET=... FARM_AUTH_URL=https://example.com ``` `AUTH_SECRET` and `BETTER_AUTH_SECRET` remain accepted as secret aliases. On Vercel the auth URL is inferred when `FARM_AUTH_URL` is absent: a production deployment uses `VERCEL_PROJECT_PRODUCTION_URL`, the project's canonical domain, and a preview uses its own `VERCEL_URL`. The deployment and branch hostnames stay trusted alongside it, so a deployment URL still works for testing. Set `FARM_AUTH_URL` explicitly when the app is served from a domain Vercel does not report, such as a proxy in front of the deployment - the auth routes reject any origin that is neither the base URL nor a trusted host. Apply the production schema before serving traffic: ```bash farm auth migrate ``` The migration command needs `DATABASE_URL`, but it does not require the runtime secret. To read the statements before they touch the database, ask for them instead of applying: ```bash farm auth migrate --dry-run farm auth migrate --write auth-schema.sql ``` Both print or save exactly what `farm auth migrate` would run and change nothing. Better Auth owns this schema, so the statements come from its migrator rather than from the table declarations a plugin makes to `farm migrate`. ## Extend with Better Auth The built-in path is intentionally opinionated, but it does not replace the existing Better Auth integration. Both approaches are supported: | Need | Configuration owner | | -------------------------------------------------------------------- | ---------------------------------------------- | | Email/password auth with Farm defaults, helpers, and hooks | Top-level `auth: true` | | Better Auth plugins, adapters, providers, callbacks, or instance API | `integrations.auth` with an app-owned instance | Choose the integration path when the application should own the complete Better Auth configuration: ```ts import { defineConfig } from "@farm.js/core"; import { betterAuth } from "@farm.js/better-auth"; import { auth } from "./src/lib/auth"; export default defineConfig({ integrations: { auth: betterAuth({ instance: auth, }), }, }); ``` This keeps working as the advanced extension path. Farm mounts the app-owned instance, while the application continues to use Better Auth's native server APIs and client. See the [Better Auth integration guide](/docs/integrations/auth/better-auth) for the complete setup. Do not configure top-level `auth` and `integrations.auth` together. Each path owns the auth catch-all route, so Farm reports a configuration error instead of choosing one implicitly. For a provider-owned UI, enterprise SSO, or another provider-specific API, choose one of the other [auth integrations](/docs/integrations/auth). --- ## Cache and PPR URL: /docs/cache-ppr Use shared runtime cache helpers, tag/path invalidation, ISR-style revalidation, and static shell caching for PPR pages. # Cache and PPR Use shared runtime cache helpers, tag/path invalidation, ISR-style revalidation, and static shell caching for PPR pages. ## Configure a shared cache Farm uses its process-local memory cache when `cache.adapter` is not configured. For multiple servers or ephemeral deployments, configure one shared adapter in `farm.config.ts`: The memory cache retains at most 1,024 entries by default and evicts the least recently used entry when it reaches that limit. Set `cache.maxEntries` to a larger positive integer when one process must keep a larger working set: ```ts import { defineConfig } from "@farm.js/core"; export default defineConfig({ cache: { maxEntries: 4_096, }, }); ``` ```bash pnpm add @farm.js/cache-redis ioredis ``` ```ts import { defineConfig } from "@farm.js/core"; import { redisCache } from "@farm.js/cache-redis"; import Redis from "ioredis"; export default defineConfig({ cache: { adapter: redisCache({ client: () => new Redis(process.env.REDIS_URL!), }), namespace: process.env.FARM_CACHE_NAMESPACE || "storefront", }, }); ``` The adapter is selected once. Routes, queries, endpoints, server functions, ISR, and PPR continue using Farm cache keys and invalidation helpers; application handlers do not import a Redis client. If framework configuration is reloaded or a test calls `configureFarmCache()` again, Farm starts a new cache generation. A fill that began under the previous adapter or namespace can still return to its original caller, but it cannot populate the newly configured cache. When a shared adapter is present, Farm uses it as the authoritative cache instead of adding an incoherent process-local front cache. The `cache` option also carries the browser-side counterpart: `cache.client.adapter` points at a client module that persists opt-in reads on the device. See [persist the client cache](/docs/api-client#persist-the-client-cache); the two adapters configure different caches on different machines and never share a store. ## Cache data **server data** ```ts import { createFarmCacheKey, getFarmDataCache } from "@farm.js/core/cache"; const cache = getFarmDataCache(); const key = createFarmCacheKey(["products", "featured"]); const products = await cache.getOrSet(key, () => fetchProducts(), { tags: ["products"], paths: ["/pricing"], revalidate: 300, }); ``` `revalidate` is measured in seconds and must be a finite, non-negative number. `0` makes an entry immediately stale, while `false` explicitly disables time-based expiry. Invalid numeric values are rejected instead of being treated as an indefinite cache lifetime. `clear()` invalidates entries and any fills already in progress. Existing callers still receive their result, but an older fill cannot repopulate the cache after it has been cleared. ## Memoize a function `unstable_cache()` wraps an async function so its results are cached and shared across requests, processes, and restarts. ```ts import { unstable_cache } from "@farm.js/core/cache"; const getProduct = unstable_cache(async (id: string) => fetchProduct(id), ["product"], { tags: ["products"], revalidate: 300, }); ``` The cache key is built from the function's identity (its name and a hash of its source), the active locale, the `keyParts` array, and the call arguments. Identity comes from the source rather than the closure instance so the key stays stable across processes and restarts — which lets a shared adapter reuse entries between server instances. Because of that, **list every variable the function closes over that is not one of its arguments in `keyParts`.** Two closures with identical source text but different captured values otherwise share a cache entry and return each other's data: ```ts // Collides: `table` is captured, not an argument, so it never reaches the key. const makeLoader = (table: string) => unstable_cache(async (id: string) => db.get(table, id)); // Correct: the captured value disambiguates the two closures. const makeLoader = (table: string) => unstable_cache(async (id: string) => db.get(table, id), [table]); ``` Values passed as call arguments already participate in the key, so they do not need to be repeated in `keyParts`. ## Revalidate **server action or route handler** ```ts import { revalidatePath, revalidateTag } from "@farm.js/core/cache"; revalidateTag("products"); revalidatePath("/pricing"); ``` `revalidatePath()` accepts a pathname or an HTTP(S) URL. Query strings and fragments do not change the cache path and are removed during normalization. ### Invalidation counts under a shared adapter The `count` reported on invalidation events (`cache.revalidateTag`, `cache.revalidatePath`) and the `ppr.shell.invalidated` event are derived from Farm's process-local entry tracking. When a shared `cache.adapter` is configured, entries and PPR shells live in the adapter rather than in local memory, so these counts reflect only what the current process has tracked — they can read as `0`, and `ppr.shell.invalidated` may not be emitted, even though the invalidation is still propagated to the adapter and applied. Treat these counts and events as best-effort observability signals rather than a confirmation of how many entries or shells were affected across a distributed deployment. ## PPR shell Partial Prerendering is experimental and disabled by default. Enable it once in `farm.config.ts`, then opt routes in individually. Without the flag, route-level PPR declarations are ignored and those routes render fully dynamically. **farm.config.ts** ```ts import { defineConfig } from "@farm.js/core"; export default defineConfig({ experimental: { ppr: true, }, }); ``` **src/app/dashboard/page.tsx** ```tsx export const experimental_ppr = true; export const revalidate = 60; export default function DashboardPage() { return
Static shell with dynamic sections
; } ``` Farm's own `export const ppr = true` and a top-of-file `"use ppr"` (or `"use ppr; 60"`) directive are equivalent opt-ins; `experimental_ppr` matches the Next.js export name. `farm explain ` reports whether a route's PPR declaration is active or ignored. When Farm serves a cached shell with dynamic holes, the browser refresh request bypasses browser and shared HTTP caches. The completed response is private and is not stored, so the refresh cannot receive the same cached shell again. The client waits for a shell's background refresh before hydrating. Farm applies the completed response's page props, route slots, manifests, locale, and deferred values first, so hydration sees the same data that produced the refreshed HTML. The background refresh reconciles the completed response into the existing shell. Stable elements keep their DOM identity, focus, text selection, form values, and scroll position while server-owned attributes and content update. If the background refresh fails, hydration still proceeds after the first attempt. Farm retries a failed refresh with backoff, at most three times and only while the browser is online, and stops as soon as hydration starts, so a retry never replaces DOM the client has taken over. ## Cache keys and tags Use stable keys for data and broad tags for invalidation. Keys identify one cached value, while tags let multiple values be refreshed together. ```ts const key = createFarmCacheKey(["products", productId]); const product = await cache.getOrSet(key, () => getProduct(productId), { tags: ["products", `product:${productId}`], paths: ["/pricing"], revalidate: 300, }); ``` ## Invalidate after writes After a mutation, invalidate the route path and any data tags that feed the page. For structured route data and [`createServerQuery`](/docs/server-queries) entries, call `invalidate(["resource", id])`. Farm uses the same route-data tag on the server and carries the invalidation to browser query and API consumers after a server action. Server functions can declare the same relationship. Farm applies the targets only after the handler succeeds and its output passes validation: ```ts import { createServerFn } from "@farm.js/core/server-fn"; export const updateProduct = createServerFn({ input: UpdateProduct, invalidates: ({ input, result }) => [ { key: ["product", input.id] }, { key: ["products", "list"] }, { tag: "products" }, { path: `/products/${result.id}` }, ], async handler({ input }) { return db.product.update({ where: { id: input.id }, data: { name: input.name }, }); }, }); ``` Endpoints accept the same `{ key }`, `{ tag }`, and `{ path }` targets. Imperative `invalidate(...)`, `revalidateTag(...)`, and `revalidatePath(...)` remain supported. Await them when calling outside a Farm action/endpoint request; Farm action requests automatically wait for registered distributed invalidations before completing. If a tag is invalidated while its value is still being generated, that caller can finish with the generated value, but Farm keeps the stored entry stale so the next read regenerates it. ## Adapter contract Custom adapters implement asynchronous entry persistence plus optional shared tag versions: ```ts import type { FarmCacheAdapter } from "@farm.js/core/cache"; export const adapter: FarmCacheAdapter = { async get(key) { return backend.get(key); }, async set(key, entry) { await backend.set(key, entry); }, async delete(key) { await backend.delete(key); }, async getTagVersions(tags) { return backend.getTagVersions(tags); }, async invalidateTags(tags) { await backend.invalidateTags(tags); }, }; ``` `getTagVersions` and `invalidateTags` coordinate invalidation without scanning every cached entry. Provider-specific adapters should use atomic increments or transactions when their backing service supports them. Lease-capable adapters return an ownership token from `acquireLease` and only release the lease when `releaseLease(key, token)` still matches that owner. For any Farm/unstorage-compatible client, `storageCacheAdapter(storage)` provides a portable baseline. `@farm.js/cache-redis` adds Redis-native atomic tag increments and regeneration leases. **src/app/api/products/route.ts** ```ts import { revalidatePath, revalidateTag } from "@farm.js/core/cache"; export async function POST(request: Request) { const input = await request.json(); await updateProduct(input); await revalidateTag("products"); await revalidatePath("/pricing"); return Response.json({ ok: true }); } ``` ## PPR with Suspense holes PPR works best when the stable page shell is outside Suspense and request-specific or slow data lives inside Suspense. As above, the route export only takes effect with `experimental.ppr` enabled in `farm.config.ts`. When a production render actually suspends, Farm currently buffers that response for late status errors and bypasses the shared PPR shell cache. This preserves fresh request-specific content instead of caching a completed response as though it were a reusable partial shell. ```tsx import { Suspense } from "react"; export const experimental_ppr = true; export const revalidate = 60; export default function BillingPage() { return (

Billing

Loading subscription...}>
); } ``` ## Observability events Cache and PPR emit events such as `cache.hit`, `cache.miss`, `cache.stale`, `cache.revalidatePath`, `cache.revalidateTag`, `ppr.shell.hit`, `ppr.shell.cached`, and `ppr.suspense.holeDetected`. Subscribe in `farm.config.ts` when debugging refresh behavior. ## Production notes - Use one namespace per application or intentionally shared cache domain. - Do not share authenticated values unless the cache key partitions by user or tenant. - Treat adapter failures as production errors; declared mutations wait for invalidation. - Use tags for data families, such as `products` or `billing`. - Use paths for route-level invalidation, such as `/pricing`. - Do not cache user-specific secrets in shared keys. - Prefer PPR for pages with a mostly stable shell and a few dynamic sections. --- ## CLI URL: /docs/cli Use the Farm CLI to run, build, generate types, migrate apps, deploy output, and add integrations. # CLI Use the Farm CLI to run, build, generate types, migrate apps, deploy output, and add integrations. ## Create an app ```bash pnpm create @farm.js/app my-app --template basic --typescript pnpm create @farm.js/app my-preact-app --template basic --renderer preact --typescript pnpm create @farm.js/app my-solid-app --template basic --renderer solid --typescript pnpm create @farm.js/app my-vue-app --template basic --renderer vue --typescript pnpm create @farm.js/app my-svelte-app --template basic --renderer svelte --typescript pnpm create @farm.js/app stripe-app --template stripe --typescript pnpm create @farm.js/app --list-templates ``` The create-app CLI includes Basic, Farm.js Auth, Better Auth, and one ready-to-configure starter for every provider supported by `farm add integration --ui`. See [Getting Started](/docs/getting-started#choose-a-starter) for the complete template catalog. React is the default renderer. `--renderer preact`, `--renderer solid`, `--renderer vue`, and `--renderer svelte` are available with the Basic starter; integration starters currently target React. See [Renderers](/docs/renderers) for setup and feature compatibility. ## Common commands | Command | Purpose | | -------------------------------- | -------------------------------------------------------------------- | | farm dev | Start the dev server. | | farm build | Build the app for the configured target. | | farm start | Run the Node server produced by farm build. | | farm upgrade --latest | Upgrade installed Farm packages to the latest stable release. | | farm upgrade --beta | Upgrade installed Farm packages to the latest beta release. | | farm doctor | Inspect a running app, or fall back to project configuration checks. | | farm doctor --offline | Check project files and config without probing a dev server. | | farm doctor --registry | Verify the public Farm beta tags against the npm registry. | | farm doctor --fix | Apply safe, additive project corrections. | | farm explain /products/42 | Explain the files and runtime behavior for one URL. | | farm agent check | Check how AI agents can read and use the running app. | | farm preview | Create a public URL for a running local app. | | farm login | Sign in to Farm Preview without starting a preview. | | farm whoami | Show which credential farm preview would use. | | farm logout | Sign out of Farm Preview on this machine. | | farm generate | Generate route/API types and integration schema artifacts. | | farm generate --check | Fail when committed generated types are stale. | | farm deploy --plan | Resolve the deployment plan without executing it. | | farm migrate inspect | Detect supported framework migration sources. | | farm migrate next --write | Apply a deterministic Next.js App Router migration. | | farm migrate tanstack --write | Apply a deterministic TanStack Router file-route migration. | | farm migrate | Run one-shot schema or provider migration commands. | | farm schema check | Check every declared table and reference against the live database. | | farm schema migrate --apply | Create every plugin's tables, dependencies first. | | farm add integration stripe --ui | Add integration wiring and optional UI. | | farm cron list | List configured UTC schedules and target routes. | | farm cron run dailyCleanup | Invoke one cron route on a running app. | | farm dev --cron | Start the dev server with the opt-in in-memory cron scheduler. | | farm telemetry status | Show the anonymous product-telemetry preference. | | farm telemetry enable | Enable anonymous Farm.js product telemetry. | | farm telemetry disable | Opt out and delete the local anonymous installation ID. | See [Anonymous telemetry](/docs/telemetry) for the exact event fields, environment overrides, and privacy guarantees. Telemetry is enabled by default for interactive local commands. ## Upgrade Farm packages ```bash farm upgrade --latest farm upgrade --beta farm upgrade --latest --dry-run ``` `--latest` selects the newest stable release published under npm's `latest` tag. `--beta` selects the newest prerelease published under the `beta` tag. Exactly one release channel is required. Farm reads the app's dependencies and upgrades every published `@farm.js/*` package together. It detects npm, pnpm, Yarn, or Bun from `packageManager` and lockfiles, preserves whether each package is a regular, development, optional, or peer dependency (including packages repeated across sections), and skips local `workspace:`, `file:`, `link:`, `portal:`, and `catalog:` references. Use `--dry-run` to inspect the commands without changing the project. ## Provider names The integration generator supports ai, stripe, supabase, workos, auth0, clerk, better-auth, authjs, autumn, polar, resend, jobs-trigger, jobs-inngest, and unkey. ## Add integrations ```bash farm add integration stripe farm add integration stripe --ui farm add integration better-auth --ui farm add integration jobs-trigger ``` Without `--ui`, the CLI installs the selected first-party provider package and its integration wiring. With `--ui`, it also installs app-owned shadcn-style components for that provider. Farm package versions follow the app's installed `@farm.js/core` version, including beta releases. ## Generate types ```bash farm generate farm generate --check ``` `farm dev` refreshes generated route/API types automatically when page and API route files change. Use `farm generate` when you want to refresh the same files outside the dev server, such as in CI, before publishing a package, or after moving files while the dev server was stopped. When integration database schemas are configured, `farm generate` also writes schema artifacts if Farm can detect the data layer. Pass `--orm` and `--output` when you want explicit schema output for a migration step. `farm generate --check` computes the route, API, environment, and i18n declarations without writing to disk. It exits non-zero and lists every missing, stale, or obsolete generated file. Use it after a normal generation step when generated types are committed: ```yaml - run: farm generate --check ``` Schema output flags such as `--orm` and `--output` cannot be combined with `--check`; database schema generation can depend on migrations and remains an explicit write operation. ## Framework migrations ```bash farm migrate inspect farm migrate next farm migrate next --write farm migrate tanstack --write ``` Framework migrations are deterministic codemods. They inspect the project, print a dry-run plan by default, and only write when `--write` is passed. Existing target files are skipped unless `--force` is passed. The Next.js migrator copies `app` or `src/app` into Farm's `src/app`, creates `farm.config.ts`, creates a minimal root layout when needed, rewrites supported imports from `next/link`, `next/navigation`, and `next/headers`, and updates package scripts to `farm dev`, `farm build`, and `node .farm/.output/server/index.mjs`. The TanStack migrator converts file routes from `src/routes` or `routes` into `src/app/**/page.tsx`, maps `$id` to `[id]`, maps `$` to `[...splat]`, adds a default export for simple `component: ComponentName` route files, and updates package scripts to `farm dev`, `farm build`, and `node .farm/.output/server/index.mjs`. Both migrators leave source files in place and print manual review notes for framework-specific APIs. ## Run command migrations ```bash farm migrate farm migrate --dry-run farm migrate --command "pnpm prisma migrate deploy" ``` `farm migrate` runs one-shot commands from `migrations.commands` in `farm.config.ts`. Use it for app-owned database migrations, integration tables, provider setup commands, and CI steps that should happen before `farm build`. **farm.config.ts** ```ts import { defineConfig } from "@farm.js/core"; export default defineConfig({ migrations: { commands: [ { name: "prisma", command: "pnpm prisma migrate deploy", }, { name: "integration schema", command: "farm generate --orm postgres --output ./schema/farm.sql", }, ], }, }); ``` Commands run sequentially and stop on the first failure. `--dry-run` prints the commands without executing them. ## Instant preview ```bash farm preview farm preview --port 3000 farm preview --name stripe-webhook farm preview --url http://localhost:4319 farm preview --dry-run ``` `farm preview` exposes the app that is already running locally. It does not build or deploy the app. Use it when a teammate, webhook provider, OAuth provider, mobile device, or browser automation tool needs a public URL for the current `farm dev` session. Farm detects the running app on common local ports, or you can pass `--port` / `--url` when the app is running somewhere specific. By default `@farm.js/tunnel` tries to open one outbound WebSocket to the hosted Farm Preview relay and returns a URL such as `https://stripe-webhook.preview.farmjs.dev`. During the relay rollout, the CLI falls back to compatibility gateway polling if the native connection is unavailable. The preview terminal logs forwarded traffic: ```txt GET /api/hello?name=something -> 200 621ms POST /api/auth/login -> 200 580ms ``` The local `farm dev` terminal logs the matched page, API route, and middleware work. Client components hydrate through the same public URL; early button clicks are queued and replayed after hydration so slow dev-mode module loading does not lose the click. `farm login`, `farm whoami` and `farm logout` manage the saved Farm Preview sign-in without starting a preview. See [Instant Preview](/docs/preview) for webhook setup, custom gateway configuration, troubleshooting, and security notes. ## Diagnose the app ```bash farm doctor farm doctor --port 4319 farm doctor --url http://localhost:4319 farm doctor --offline farm doctor --offline --registry farm doctor --fix farm doctor --json ``` `farm doctor` first probes the running app at `http://localhost:3000`. A live app provides the most accurate result because Farm can report its resolved routes, API methods, middleware, integrations, storage mounts, schedules, workflows, layers, and deployment runtime. When no app is running, the command falls back to config and filesystem checks. Pass `--port` or `--url` when the app runs somewhere else. An explicitly requested runtime that cannot be reached is reported as a warning before the project checks. Use `--offline` to skip the runtime probe. Add `--registry` to verify that the public `@farm.js/core` and `@farm.js/create-app` beta tags resolve and to warn when an exact Farm dependency is stale. Project checks cover Node and pnpm support, package-manager and workspace resolution, generated type freshness, selected renderer and integration entrypoints, routing, deployment, storage, and schedules. The command is read-only by default: it does not install dependencies, run package-manager scripts, update generated files, or contact providers. Registry access is opt-in. The command exits with a non-zero status only when a check fails. Warnings keep a zero exit status, so CI can distinguish broken configuration from production-readiness advice. `--json` prints the complete report without terminal formatting and preserves the same exit-code contract. `farm doctor --fix` applies only corrections Farm can make without replacing application code. Today that means creating a missing `src/app/layout.tsx`; an existing file is never overwritten. The command reruns diagnostics after each correction and reports exactly which files it created. See [Doctor](/docs/doctor) for diagnostics, the JSON contract, and CI examples, and [Built-in DevTools](/docs/devtools) for the browser dashboard. ## Check agent readiness ```bash farm agent check farm agent check https://acme.com farm agent check http://localhost:3000/docs --path /getting-started farm agent check --mcp /api/tools --json ``` `farm agent check` probes a running app over HTTP the way an AI agent meets it, so it works the same against `farm dev`, `farm start`, or a deployed site. It defaults to `http://localhost:3000`; a URL with a path is treated as the app's `basePath`, and `--path` picks the page to check. | Check | Passes when | | ------------------- | ----------------------------------------------------------------------------------------------------- | | Page metadata | The page has a title, description, canonical link, and `lang`; JSON-LD is reported separately. | | Markdown mirror | The page advertises its `.md` mirror, the mirror answers, and a missing `.md` answers a Markdown 404. | | Content negotiation | `Accept: text/markdown` on the page URL returns Markdown. Prerendered pages report this as info. | | Partial reads | The mirror serves `?outline`, `?section`, `?max_tokens`, and ETags. | | llms.txt | `/llms.txt` lists pages, and `/llms-full.txt` answers. | | Crawlers | `/robots.txt` exists and does not block every agent; `/sitemap.xml` answers. | | API catalog | `/.well-known/api-catalog` is an RFC 9727 Linkset, and the OpenAPI spec it lists answers. | | MCP | The MCP endpoint (from the catalog, `--mcp`, or `/api/mcp`) answers a 401 with `WWW-Authenticate`. | A surface that is off is reported as info or a warning with the config that turns it on, such as `agent: { llmsTxt: true }`. A surface that is advertised but broken, such as an OpenAPI spec listed in the catalog that does not answer, fails, and the command exits with code 1. `--json` prints the report for CI. The MCP check sends one unauthenticated `initialize` request; every other check is a `GET` or `HEAD`. ## Explain a route ```bash farm explain /products/42 farm explain /products/42 --json ``` `farm explain` resolves a URL against the app router without starting the app. It reports: - the matching page file, route pattern, parameters, and source layer; - inherited layouts and file- or config-based middleware; - route-rule and module runtime controls, rendering mode, PPR, and cache settings; - static or generated metadata and the nearest Open Graph and Twitter images; - the deployment target, preset, runtime compatibility, and actionable warnings. Request paths are not required to contain valid percent escapes. `farm explain` keeps a malformed segment raw, matching the runtime router, instead of aborting the diagnostic with a URI error. Use the text output while debugging and `--json` for tooling. The command is read-only and fails when no page route matches the supplied URL. ## Build ```bash farm build ``` `farm build` respects `deploy.target`, `output`, and provider-specific output options from `farm.config.ts`. ## Plan a deployment ```bash farm deploy --plan farm deploy --vercel --prod --plan ``` `farm deploy --plan` resolves the target, Nitro preset, runtime, output directory, and exact build and provider commands. It does not build, inspect build output, check credentials, or call a deployment CLI. This makes it safe to use during review and in CI policy checks. ## Cron commands ```bash farm cron list farm cron list --json farm cron run dailyCleanup farm cron run dailyCleanup --url http://localhost:4319 farm dev --cron ``` `farm cron run` reads the named schedule from `farm.config.ts`, sends GET to its configured route, and forwards `CRON_SECRET` as bearer authorization. `farm dev --cron` runs the same routes on their UTC schedules in memory, prevents overlap inside the local process, and stops with the dev server. See [Cron](/docs/cron) for configuration, production adapters, security, and reliability semantics. ## Production notes - Run `farm generate` in CI if generated route/API types are not committed. - Run `farm migrate` before `farm build` when schema changes are deployed separately from app code. - Use `farm build` before deployment to catch docs, API, route, and integration wiring issues. - Prefer `farm add integration --ui` only when you want generated UI files. - Review generated integration files before committing them. --- ## Configuration URL: /docs/configuration Use farm.config.ts as the single project control plane for source paths, integrations, docs, KV storage, database clients, deployment, and framework behavior. # 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 **farm.config.ts** ```ts 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: ```ts 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: ```json title="package.json" { "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](/docs/renderers) for the feature matrix and dedicated [React](/docs/renderers/react), [Preact](/docs/renderers/preact), [Solid](/docs/renderers/solid), [Vue](/docs/renderers/vue), and [Svelte](/docs/renderers/svelte) guides. ### Preact Install Preact and its FARMJS renderer adapter: ```bash pnpm add @farm.js/preact preact ``` ```ts import { 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](/docs/renderers/preact) guide for typed server calls and compatibility boundaries. Create a ready-to-run Preact application from the CLI: ```bash pnpm create @farm.js/app my-preact-app --template basic --renderer preact --typescript ``` ### Svelte Install the Svelte adapter and runtime: ```bash pnpm add @farm.js/svelte svelte ``` ```ts import { defineConfig } from "@farm.js/core"; import { svelte } from "@farm.js/svelte"; export default defineConfig({ renderer: svelte(), }); ``` Routes can then use Svelte 5 components directly: ```text src/app/layout.svelte src/app/page.svelte src/app/products/[id]/page.svelte ``` See [Svelte Renderer](/docs/renderers/svelte) for module route exports, layout snippets, hydration, typed server calls, and current compatibility boundaries. Create a ready-to-run Svelte application from the CLI: ```bash pnpm create @farm.js/app my-svelte-app --template basic --renderer svelte --typescript ``` ### Vue Install Vue and its FARMJS renderer adapter: ```bash pnpm add @farm.js/vue vue ``` ```ts import { 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: ```text src/app/layout.vue src/app/page.vue src/app/products/[id]/page.vue ``` FARMJS 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 ``. See the [Vue server-rendering guide](https://vuejs.org/guide/scaling-up/ssr) for Vue-specific SSR constraints. See [Vue Renderer](/docs/renderers/vue) for SFC route exports, hydration, typed server calls, and current compatibility boundaries. Create a ready-to-run Vue application from the CLI: ```bash pnpm create @farm.js/app my-vue-app --template basic --renderer vue --typescript ``` ### Solid Install the Solid adapter and runtime: ```bash pnpm add @farm.js/solid solid-js ``` ```ts import { 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](/docs/renderers/solid) for route conventions, client boundaries, typed server calls, and current compatibility boundaries. Create a ready-to-run Solid application directly from the CLI: ```bash pnpm create @farm.js/app my-solid-app --template basic --renderer solid --typescript ``` Omitting `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`: ```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](/docs/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. ```ts 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: ```ts title="farm.config.ts" 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: ```ts title="farm.config.ts" 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 `` or `` 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: ```ts 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: ```ts title="farm.config.ts" 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](/docs/plugins/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](/docs/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: ```ts title="farm.config.ts" 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. ```ts title="farm.config.ts" 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](/docs/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`](https://llmstxt.org): a Markdown index of every static page, with each page's metadata title and description, linking to its [Markdown mirror](/docs/markdown) 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](/docs/routing#application-metadata-routes) 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](/docs/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. ```tsx title="src/app/blog/[slug]/page.tsx" 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 ` ``` ### Phase 3: pages, one route at a time Create the Farm folder and `page.vue`, add explicit imports, convert `definePageMeta`, navigation, and data APIs using the parity tables. Keep dynamic segment names identical. **Verify each page** in `pnpm dev`: status and HTML match the Nuxt recording; the browser console has no `Failed to resolve component` or hydration warnings. ### Phase 4: server routes, server middleware, mutations Convert `server/api/**`, then `server/routes/**`, `server/middleware/**`, `server/plugins/**`. **Verify** every endpoint with `curl -i` using the recorded method, body, and cookies: status, headers, `Set-Cookie`, and body match. `farm generate` lists the routes in `src/lib/api.generated.ts`. ### Phase 5: route middleware, auth, config, modules Convert `app/middleware`, `runtimeConfig`, `app.config.ts`, `nuxt.config.ts`, and modules. **Verify:** protected URLs redirect when signed out; `routeRules` headers and redirects appear in `curl -i` output. ### Phase 6: production build Run `pnpm build` then `pnpm start`. **Verify** the Phase 3 and 4 checks against the production server, including SPA navigation between two hydrated routes and the state that must survive it. ### Phase 7: type-check and remove Nuxt Run `pnpm type-check`. Remove `nuxt` and every `@nuxt/*`, `@nuxtjs/*`, `@pinia/nuxt`, and `nuxt-*` package, plus `nuxt.config.ts`, `app.config.ts`, `.nuxt/`, `.output/`, `app/`, `server/`, and Nuxt deployment files. This search must return nothing; then rerun Phase 6: ```bash grep -rn --include='*.vue' --include='*.ts' -e "#app" -e "#imports" -e "#components" -e "definePageMeta" -e "defineNuxt" -e "useNuxtApp" -e "useRuntimeConfig" -e "defineEventHandler" src ``` ### Failure symptoms and recovery | Symptom | Cause | Recovery | | ----------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `farm dev` loads Nuxt or unknown Vite plugins | Leftover root `vite.config.*` | Delete it; move needed options into `farm.config.ts` `vite`. | | `ReferenceError: ref is not defined` or `definePageMeta is not defined` | Nuxt auto-import or macro left in place | Add the explicit `vue` import; delete the macro and use the Farm export. | | `[Vue warn]: Failed to resolve component: ProductCard` | Auto-imported component | Import the SFC explicitly in ` ``` The page lives at `src/app/(shop)/products/[id]/page.vue`, the layout at `src/app/(shop)/layout.vue`, the guard at `src/app/(shop)/middleware.ts`. Make 404 and redirect decisions in `middleware.ts`, not in ` ``` In routes that do not hydrate, `AppLink` renders a working `` (document navigation). ## Route middleware Canonical docs: [/docs/middleware](/docs/middleware). | Nuxt | Farm | Status | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------- | | named `middleware/auth.ts` + `definePageMeta` | `middleware.ts` in the protected folder or route group | `different` | | global `middleware/*.global.ts` | `src/app/middleware.ts` or `farm.config.ts` `middleware: [{ matcher, handler }]` | `different` | | inline middleware in `definePageMeta` | `middleware.ts` next to the page | `different` | | `defineNuxtRouteMiddleware((to, from) => ...)` | `export async function middleware(request: Request, context: RequestMiddlewareContext)` (type from `@farm.js/core/middleware`) | `different` | | `to.params`, `to.path`, `to.query` | `context.params`, `context.pathname`, `context.searchParams` | `rename` | | `return navigateTo("/login")` | `return Response.redirect(new URL("/login", request.url))` or `context.redirect("/login")` | `different` | | `return abortNavigation()` | `return new Response("Forbidden", { status: 403 })` | `different` | | client-side-only middleware | None; Farm middleware runs on the server | `none` | Farm middleware runs on the server, in development and production, for config entries first, then `src/app/**/middleware.ts` from the root segment down. ```ts // src/app/(account)/middleware.ts import type { RequestMiddlewareContext } from "@farm.js/core/middleware"; import { notFound } from "@farm.js/core/navigation"; import { readSession } from "@/server/session"; export async function middleware(request: Request, context: RequestMiddlewareContext) { const session = await readSession(request); if (!session) return Response.redirect(new URL("/sign-in", request.url)); if (context.params.id === "0") notFound(); context.data.set("user.name", session.user.name); context.set("session", session); } ``` The handler continues automatically unless it returns a `Response`. `context.data` values are client-safe and reach the page as `props.middleware?.data.get("user.name")`. Values from `context.set()` stay on the server; read them with `getMiddlewareContext()` from `@farm.js/core/middleware`. Repeat authorization in API routes. `context.cookies.set()` and `context.headers.set()` add response cookies and headers. ## Data fetching Canonical docs: [/docs/api-routes](/docs/api-routes), [/docs/api-client](/docs/api-client), [/docs/server-queries](/docs/server-queries), [/docs/environment-functions](/docs/environment-functions). | Nuxt | Farm | Status | | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ----------- | | `useFetch`/`useAsyncData` in a non-interactive page | `await` a server module in ` ``` Wrap server data access with `createServerOnlyFn` from `@farm.js/core/environment` so a module imported by a route that later hydrates cannot ship its implementation to the browser. `useServerQuery` and `useAction` from `@farm.js/vue/bindings` exist, but calling a server query or server function from browser code requires the server-function transform (`@farm.js/plugin/rsc` with `experimental.serverActions: true`). That path is not documented for Vue; prefer API routes plus `apiClient`. Do not import `@farm.js/core/server-query/client` in Vue code: it imports React. ## State | Nuxt | Farm | Status | | ------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------- | | `useState(key, init)` | None. Browser-only shared state: a module-level `ref` in a composable used by hydrated routes | `none` | | server-to-client state transfer | None generic; pass client-safe values through middleware `context.data` or fetch after mount | `none` | | Pinia via `@pinia/nuxt` | `app.use(createPinia())` in the app setup module (`vue({ app })`); stores start fresh in the browser | `different` | | `provide`/`inject` (Vue) | `provide()` in a layout's ` ``` Browser-only helpers: `createClientOnlyFn` from `@farm.js/core/environment` throws a clear error if called during SSR. ## Auto-imports Nuxt auto-imports are `none` in Farm. Every identifier must be imported explicitly. | Nuxt auto-import | Farm import | | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | `ref`, `computed`, `reactive`, `watch`, `watchEffect`, `onMounted`, `nextTick`, `toRef(s)`, `provide`, `inject`, `defineAsyncComponent` | `import { ... } from "vue"` | | `defineProps`, `defineEmits`, `defineOptions`, `defineModel` | Compiler macros; no import needed (`same`) | | `app/components/**` (including `` from `components/base/Button.vue`) | `import BaseButton from "@/components/base/Button.vue"` | | `app/composables/**`, `app/utils/**` | `import { useCart } from "@/composables/useCart"` | | `server/utils/**` | `import { db } from "@/server/db"` | | `shared/**` | `import { slugify } from "@/shared/slugify"` | | `#imports`, `#app`, `#components` | Delete; import from the real module | Find the gaps with `pnpm type-check` (`Cannot find name 'ref'`) and the browser console (`Failed to resolve component`). Nuxt resolves nested component names by directory prefix; keep the template name and import the file under that name. ## Head and SEO Canonical docs: [/docs/routing](/docs/routing) (Metadata And OG Images). | Nuxt | Farm | Status | | -------------------------------------------- | ------------------------------------------------------------------------------------- | ----------- | | `app.head` in `nuxt.config.ts` | `export const metadata` in `src/app/layout.vue` | `different` | | `useHead` / `useSeoMeta` (static) | `export const metadata: Metadata` (type from `@farm.js/core`) in ` ``` Production responses show a generic message; details stay in server logs. A `reset()` button needs the boundary to hydrate. ## Plugins Canonical docs: [/docs/plugins/client](/docs/plugins/client), [/docs/project-structure](/docs/project-structure). | Nuxt | Farm | Status | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ----------- | | `defineNuxtPlugin` calling `nuxtApp.vueApp.use(plugin)` | `app.use(plugin)` in the app setup module (`vue({ app })`) | `different` | | `provide: { helper }` / `useNuxtApp().$helper` | Export the helper from a module and import it | `different` | | global directives (`vueApp.directive`) | `app.directive()` in the app setup module, or register locally in ` {@render children?.()} ``` Remove `hydrate` from the root layout later for routes that need no client JavaScript, and add it to the pages and layouts that do. Every `export const` route option (`hydrate`, `metadata`, `generateMetadata`, `dynamic`, `revalidate`, `generateStaticParams`, `runtime`) goes in `

{post.title}

``` Client-fetched alternative for a hydrated page, which also survives SPA navigation: ```svelte ``` ## Feature parity: endpoints (`+server.ts`) | SvelteKit | Farm | Status | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------- | ----------- | | `export const GET: RequestHandler` | `export async function GET(request: Request)` (plain handler, no params) | `different` | | handler `{ params, request, url }` | `export const GET = createEndpoint(async ({ params, request, query, body }) => ...)` from `@farm.js/core/api` | `different` | | `json(data)` | `Response.json(data)` | `rename` | | `error(400, msg)` | declared `errors` plus `error("code", data)` in `createEndpoint`, or `Response.json(..., { status })` | `different` | | `redirect(303, url)` | `return Response.redirect(new URL(url, request.url), 303)` | `different` | | `fallback` export | export each method; unsupported methods return 405 with `Allow` | `none` | | `locals` in handler | endpoint `middleware: [fn]` returning context objects (`/docs/api-routes`) | `different` | | `platform` | not verified for Farm targets | `none` | | `getClientAddress()` | not exposed; enable `server.trustProxy` and read forwarding headers | `none` | ```ts // src/app/api/posts/[id]/route.ts import { createEndpoint } from "@farm.js/core/api"; export const GET = createEndpoint(async ({ params }) => { const post = await getPost(String(params.id)); return post ? Response.json(post) : Response.json({ message: "Not found" }, { status: 404 }); }); ``` ## Feature parity: form actions and `$app/forms` | SvelteKit | Farm | Status | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------- | ----------- | | default action (`actions.default`) | `POST` in `src/app/api//route.ts`; `
` | `different` | | named actions (`?/create`) | one API route per action, or one route that switches on a form field | `different` | | `fail(400, data)` | redirect back with a query value for no-JS, JSON `400` for enhanced requests | `different` | | `form` prop / `ActionData` | read the `search` prop (no-JS) or local `$state` (enhanced) | `different` | | `use:enhance` | `onsubmit` handler that `fetch`es the same endpoint; requires `hydrate = true` | `different` | | `applyAction`, `deserialize` | none | `none` | | progressive enhancement | native form post plus `303` redirect works without JavaScript | `same` | | `createAction` (Svelte binding) | requires a browser-callable server function, which needs the React-only `experimental.serverActions` transform | `none` | ```ts // src/app/api/todos/route.ts export async function POST(request: Request) { const origin = request.headers.get("origin"); if (origin && origin !== new URL(request.url).origin) return new Response(null, { status: 403 }); const title = String((await request.formData()).get("title") ?? "").trim(); const wantsJson = request.headers.get("accept")?.includes("application/json"); const back = new URL("/todos", request.url); if (!title) { if (wantsJson) return Response.json({ error: "Title is required" }, { status: 400 }); back.searchParams.set("error", "Title is required"); return Response.redirect(back, 303); } await createTodo(title); return wantsJson ? Response.json({ ok: true }) : Response.redirect(back, 303); } ``` Behind a proxy, `request.url` reflects the public origin only with `server.trustProxy: true`. ```svelte {#if error}

{error}

{/if}
``` ## Feature parity: remote functions (`$app/server`) | SvelteKit (experimental) | Farm | Status | | ------------------------ | ---------------------------------------------------------------------- | ----------- | | `query()` | `GET` API route with a `query` schema, called with `apiClient` | `different` | | `query.batch()` | none; accept an array in one endpoint | `none` | | `command()` | `POST` API route with a `body` schema, called with `apiClient` | `different` | | `form()` | API route plus native form (forms section) | `different` | | `prerender()` | none; a `GET` route with `cache-control` headers or `routeRules` `swr` | `none` | | `getRequestEvent()` | `getCurrentRequest()` from `@farm.js/core/request` | `different` | | `read()` | none; read files with `node:fs` in server code | `none` | `createServerFn` and `createServerQuery` exist in Farm, but browser callers need the React server components transform. In a Svelte app, call them from API routes or other server code only. ## Feature parity: `$app/navigation` Create one router store per component: `const router = createRouter()` from `@farm.js/svelte/bindings`. It only works in hydrated routes and resolves `basePath` for you. | SvelteKit | Farm | Status | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | | `goto(url, opts)` | `router.push(url, { scroll, state, viewTransition })`; `router.replace(url)` for `replaceState: true` | `rename` | | `invalidate`, `invalidateAll` | `router.refresh()` (see load table) | `different` | | `preloadData`, `preloadCode` | `router.prefetch(url)` | `rename` | | `beforeNavigate` | `getRouter().addBlocker(({ from, to, action }) => shouldBlock)` exists only in `@farm.js/core/client`, which imports React. Use it only if `react` is installed; otherwise guard your own `router.push` calls and use a `beforeunload` listener. | `none` | | `afterNavigate` | `$effect` on `$router.navigation.state === "idle"` or on `$router.pathname` | `different` | | `onNavigate` (view transitions) | `router.push(url, { viewTransition: true })` | `different` | | `disableScrollHandling` | `router.push(url, { scroll: false })` | `different` | | `pushState`, `replaceState` (shallow routing) | `router.pushState(state, href?)`, `router.replaceState(state, href?)`; read `$router.pageState`. Argument order is state first. | `rename` | | `data-sveltekit-preload-data` | `onmouseenter={() => router.prefetch(href)}` | `different` | | `data-sveltekit-reload` | plain `
` (already a document load) | `same` | | automatic link interception | none; use a link component | `none` | ```svelte void router.prefetch(href)}> {@render children?.()} ``` With `basePath` set, the `href` attribute needs the prefix even though `router.push` adds it. ## Feature parity: `$app/state` and `$app/stores` | SvelteKit | Farm | Status | | --------------------------- | ----------------------------------------------------------------------- | ----------- | | `page.params` | `params` prop (pages, layouts, error) | `different` | | `page.url.pathname` | `path` prop (SSR-safe); `$router.pathname` after hydration | `different` | | `page.url.searchParams` | `search` prop (SSR-safe object); `$router.searchParams` after hydration | `different` | | `page.data` | `middleware?.data` prop | `different` | | `page.form` | none (forms section) | `none` | | `page.error`, `page.status` | `error` prop in `error.svelte`; no status value | `different` | | `page.state` | `$router.pageState` | `rename` | | `page.route.id` | none | `none` | | `navigating` | `$router.navigation` (`state`, `pending`, `from`, `to`, `action`) | `rename` | | `updated` | none | `none` | `createRouter()` reports `pathname: "/"` and empty params on the server; never render them in SSR. ## Feature parity: environment, paths, and `$env` | SvelteKit | Farm | Status | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | `browser` (`$app/environment`) | `!import.meta.env.SSR` or `typeof window !== "undefined"` | `different` | | `dev` | `import.meta.env.DEV` | `different` | | `building` | none | `none` | | `version` | none | `none` | | `base` (`$app/paths`) | export a constant used by both `farm.config.ts` `basePath` and components | `none` | | `assets`, `resolve()`, `asset()` | none; reference `/file` from `public/` | `none` | | `$env/static/private` | `env.server` schema in `farm.config.ts`; `getEnv("NAME")` from `@farm.js/core/env` (throws in the browser) | `rename` | | `$env/dynamic/private` | `process.env.NAME` in server-only code (Node targets) | `different` | | `$env/static/public` | `env.public` schema; `getPublicEnv("NAME")` (inlined at build) | `rename` | | `$env/dynamic/public` | none; public values are build-time. Serve runtime config from an API route. | `none` | | `$lib/server` boundary | `createServerOnlyFn` from `@farm.js/core/environment` (`/docs/environment-functions`) and never importing server modules from `.svelte` files | `different` | Declare schemas in `farm.config.ts`, for example `env: { server: { DATABASE_URL: z.string().url() }, public: { PUBLIC_SITE_NAME: z.string() } }`. Farm does not require a `PUBLIC_` prefix; the `public` block decides what reaches the browser. ## Feature parity: hooks and locals | SvelteKit | Farm | Status | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ----------- | | `handle` (auth, headers) | `src/app/middleware.ts` or `middleware` array in `farm.config.ts` (`/docs/middleware`) | `different` | | `sequence(a, b)` | several config middleware entries, then nested `middleware.ts` files, in that order | `different` | | `resolve(event, { transformPageChunk })` | none | `none` | | `event.locals` (server-only) | `context.set()` / `ctx.locals.set()`; read with `getMiddlewareContext()` from `@farm.js/core/middleware` | `rename` | | locals values the page renders | `ctx.data.set()` (serialized into the page) | `different` | | `handleFetch` | none; `api` calls endpoints in process, `apiClient` has lifecycle hooks (`/docs/api-client`) | `none` | | `handleError` (server) | `observability.onEvent` for `request.error`, `render.error`, `api.error` (`/docs/observability`) plus `error.svelte` | `different` | | `handleError` (client) | none; `window` `error` and `unhandledrejection` listeners in the root layout `onMount` | `none` | | `reroute` | `ctx.rewrite("/path")` in middleware | `different` | | `init` | `register()` in `src/instrumentation.ts` | `different` | | `transport` | none; props and middleware data must be JSON-serializable | `none` | A `handle` hook that loads `event.locals.user` and redirects anonymous `/admin` visitors becomes: ```ts // src/app/admin/middleware.ts import type { RequestMiddlewareContext } from "@farm.js/core/middleware"; export async function middleware(request: Request, context: RequestMiddlewareContext) { const user = await getUser(context.cookies.get("session")); if (!user) return Response.redirect(new URL("/login", request.url), 303); context.set("user", user); // server-only context.data.set("userName", user.name); // visible to the page } ``` Repeat authorization inside every API route; middleware is not the only boundary. ## Feature parity: errors and redirects from `@sveltejs/kit` | SvelteKit | Farm | Status | | --------------------------- | --------------------------------------------------------------------------------------------------- | ----------- | | `error(404)` | `notFound()` from `@farm.js/core/navigation` in middleware (renders `not-found.svelte`, status 404) | `different` | | `error(403, msg)` elsewhere | API: `Response.json({ message }, { status: 403 })`; middleware: return a `Response` | `different` | | `redirect(status, url)` | `redirect(url, status)` (301, 302, 303, 307, 308) in middleware; `Response.redirect` in API routes | `different` | | thrown errors in components | nearest `error.svelte` | `same` | | `isRedirect`, `isHttpError` | `isFarmRedirectError`, `isFarmNotFoundError` from `@farm.js/core/navigation` | `rename` | Prefer calling `redirect()` and `notFound()` from middleware. Calls inside a Svelte component script are not covered by Farm's Svelte renderer tests. ## Feature parity: adapters and deployment | SvelteKit adapter | Farm `deploy` in `farm.config.ts` (`/docs/deployment`) | Status | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | `adapter-auto` | omit `deploy`; Farm detects `VERCEL`, `NETLIFY`, `CF_PAGES` | `same` | | `adapter-node` | `target: "node"`; run `node .farm/.output/server/index.mjs` | `rename` | | `adapter-vercel` | `target: "vercel"`; output `.vercel/output` | `rename` | | `adapter-netlify` | `target: "netlify"`; output `dist` plus `.netlify/functions-internal` | `rename` | | `adapter-cloudflare` (Pages) | `target: "cloudflare"` (`cloudflare-pages` preset) | `rename` | | `adapter-cloudflare` (Workers) | `preset: "cloudflare-module"` | `different` | | `adapter-static` | none as a full static export. Prerender routes and deploy a server target, or try a static Nitro preset such as `github-pages` (unverified). | `none` | | other community adapters | `preset: ""` | `different` | ## Feature parity: head, styling, and other features | SvelteKit | Farm | Status | | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | `` | ``, carried through the Svelte server adapter | `same` | | per-page `` and meta | `export const metadata: Metadata` or `generateMetadata` in the module script | `same` | | scoped `<style>` | unchanged | `same` | | global CSS in `+layout.svelte` | `import "./globals.css"` in the root layout module script | `same` | | Tailwind via `vite.config` | Farm enables the installed `@tailwindcss/vite` itself unless the project has a PostCSS config | `different` | | runes, stores, actions, transitions, snippets | unchanged | `same` | | snapshots (`export const snapshot`) | none; keep state in `router.replaceState(state)` or `sessionStorage` | `none` | | `src/service-worker.ts`, `$service-worker` | `@farm.js/pwa` (`/docs/plugins/pwa`); a custom worker is copied unbundled, so ship JavaScript | `different` | | i18n (Paraglide, `[[lang]]`) | `i18n` config plus `createI18n()` from `@farm.js/svelte/bindings` (`/docs/internationalization`), or keep Paraglide's Vite plugin in `vite.plugins` | `different` | | unit tests (Vitest) | `vitest.config.ts` with `svelte()` from `@sveltejs/vite-plugin-svelte`; server logic with `createFarmTestHarness` from `@farm.js/core/testing` (`/docs/testing`) | `different` | | Playwright e2e | `webServer.command: "pnpm build && pnpm start"`, port 3000 | `different` | ## Final verification checklist 1. `pnpm dev`: every baseline URL returns its baseline status; interactive routes hydrate without console errors; layout state survives client navigation through the link component. 2. Forms work with JavaScript disabled and enabled, including validation errors and redirects. 3. `pnpm build` succeeds and `pnpm start` serves the same statuses, headers, and cookies. 4. `pnpm type-check` (`svelte-check`) passes and `pnpm exec farm generate` leaves no diff. 5. No source file imports `@farm.js/core/client` (in Svelte code), `$app/*`, `$env/*`, or `@sveltejs/kit`. 6. No private env value appears anywhere under `.farm/.output/public`. See [Svelte Renderer](/docs/renderers/svelte), [Routing](/docs/routing), [Middleware](/docs/middleware), [API Routes](/docs/api-routes), and [Deployment](/docs/deployment) for the full Farm contracts. --- ## Migrate from TanStack Start URL: /docs/migrations/tanstack Move a TanStack Start app to Farm: file routes, the root route, loaders, server functions, and server routes. # Migrate from TanStack Start to Farm This document is the complete instruction set for a coding agent that is asked to move a TanStack Start (or TanStack Router file-route) React app to Farm. It contains a phased procedure with verification gates, then a feature-parity reference that maps every TanStack Start and TanStack Router feature to its Farm file convention, import path, and status. Do the migration by hand from this document: do not run `farm migrate`. The procedure was verified by migrating the official starter (`@tanstack/react-start` 1.169.2, created with `@tanstack/cli create`) to Farm 0.1.14. ## Task contract - Keep the TanStack app runnable until Farm passes dev, build, start, and type-check. Work on a branch; never delete `src/routes`, `src/router.tsx`, or TanStack packages before the last phase. - Convert feature by feature using the parity tables. Every Farm API named here exists in Farm 0.1.14; do not invent options. When a row says `none`, use its workaround or report the gap. - Status values: `same` (works unchanged), `rename` (same behavior, new name or path), `different` (rewrite needed, as described), `none` (no Farm equivalent; use the workaround). - React is built into `@farm.js/core`. A React app installs no renderer package. - Canonical Farm docs are linked as relative URLs on `https://farmjs.dev`, for example `/docs/routing`. ## Migration procedure ### Phase 0: prerequisites 1. Node `>=22.13.0` (the `engines` range of `@farm.js/core` 0.1.14). 2. `git status` is clean. Create a branch. 3. Record the baseline: list every route file in `src/routes`, every `createServerFn`, every `server: { handlers }` or `createServerFileRoute`/`createAPIFileRoute`, every `createMiddleware`, and the scripts in `package.json`. Run the TanStack app once and note each route's status code. ### Phase 1: add Farm next to TanStack 1. Add exact versions of the current release (`npm view @farm.js/core version`): `@farm.js/core@<version>` to `dependencies` and `@farm.js/cli@<version>` to `devDependencies`. Do not use `latest`: under pnpm 11+, `minimumReleaseAge` can resolve `latest` to an older Farm release for a day after a publish. If installs are blocked by the release age, add `'@farm.js/*'` to `minimumReleaseAgeExclude` in `pnpm-workspace.yaml`. 2. Create `farm.config.ts` with `export default defineConfig({})`, importing `defineConfig` from `@farm.js/core`. 3. Replace scripts. TanStack has no `start` script by default; add one: ```json "scripts": { "dev": "farm dev", "build": "farm build", "start": "node .farm/.output/server/index.mjs", "type-check": "tsc --noEmit" } ``` `farm start` also works locally, but `@farm.js/cli` is a devDependency, so hosts that prune devDependencies cannot run it. Keep the `node` command as the `start` script, the same as every `create @farm.js/app` template. Remove `generate-routes` (`tsr generate`) and `vite preview`. 4. Add `.farm` to `.gitignore`. Install. ### Phase 2: remove the TanStack Vite config `farm dev` creates its Vite server without `configFile: false`, so Vite auto-loads a root `vite.config.*` and merges its plugins with Farm's. Both production Vite passes in `farm build` (client and server) do the same. The starter's config adds `tanstackStart()`, `viteReact()`, and the TanStack devtools plugin. Farm's React renderer adds no `@vitejs/plugin-react`, so the leftover plugin breaks hydration on every route with "@vitejs/plugin-react can't detect preamble". 1. Delete `vite.config.ts` (or move it out of the project root for reference). 2. Move only options you still need into `farm.config.ts` under `vite`. The `vite` object is merged in `farm dev` and in both `farm build` passes, except the build options Farm owns (see `/docs/configuration#vite-config`). 3. Never re-add `tanstackStart()`, `@vitejs/plugin-react`, the TanStack devtools Vite plugin, Nitro or hosting Vite plugins, or `tailwindcss()` from `@tailwindcss/vite` (Farm enables Tailwind itself; keep the `@tailwindcss/vite` package installed). 4. `resolve: { tsconfigPaths: true }` is not needed for the starter: Farm aliases `@/` to the source directory, and the starter's `#/*` alias is a package.json `"imports"` entry that Vite and Node resolve natively. Keep `"imports": { "#/*": "./src/*" }` and the matching tsconfig `paths`. Gate: `pnpm dev` starts on port 3000 without Vite plugin errors. ### Phase 3: convert `__root.tsx` into `src/app/layout.tsx` Farm does not read `__root.tsx`. Without this phase every route loses the global stylesheet, the header and footer, and the document title. Farm supports a root layout that returns the full `<html>` document. Before (starter): ```tsx // src/routes/__root.tsx import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router"; import { TanStackDevtools } from "@tanstack/react-devtools"; // plus the router devtools panel import Footer from "../components/Footer"; import Header from "../components/Header"; import appCss from "../styles.css?url"; const THEME_INIT_SCRIPT = `(function(){ /* reads localStorage, sets html class */ })();`; export const Route = createRootRoute({ head: () => ({ meta: [ { charSet: "utf-8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, { title: "TanStack Start Starter" }, ], links: [{ rel: "stylesheet", href: appCss }], }), shellComponent: RootDocument, }); function RootDocument({ children }: { children: React.ReactNode }) { return ( <html lang="en" suppressHydrationWarning> <head> <script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} /> <HeadContent /> </head> <body className="font-sans antialiased [overflow-wrap:anywhere] selection:bg-[rgba(79,184,178,0.24)]"> <Header /> {children} <Footer /> <TanStackDevtools config={{ position: "bottom-right" }} plugins={ [ /* router panel */ ] } /> <Scripts /> </body> </html> ); } ``` After: ```tsx // src/app/layout.tsx import type { LayoutProps, Metadata } from "@farm.js/core"; import Footer from "../components/Footer"; import Header from "../components/Header"; import "../styles.css"; const THEME_INIT_SCRIPT = `(function(){ /* unchanged */ })();`; export const metadata: Metadata = { title: "TanStack Start Starter", }; export default function RootLayout({ children }: LayoutProps) { return ( <html lang="en" suppressHydrationWarning> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} /> </head> <body className="font-sans antialiased [overflow-wrap:anywhere] selection:bg-[rgba(79,184,178,0.24)]"> <Header /> {children} <Footer /> </body> </html> ); } ``` Rules: import CSS directly (drop `?url` and the stylesheet `links` entry); delete `HeadContent`, `Scripts`, and the TanStack devtools (Farm injects its head tags, assets, and client runtime before `</head>` and `</body>`); move `meta` title and description into `metadata`. `Header` uses TanStack `Link`, so convert it in Phase 5 before checking this phase in a browser. Gate: `/` returns 200 with styles, header, footer, and the title. ### Phase 4: move routes into `src/app` 1. Map each file with the [route naming table](#file-route-naming). Verified mappings from the starter: `index.tsx` to `src/app/page.tsx`, `about.tsx` to `src/app/about/page.tsx`, `posts.$postId.tsx` to `src/app/posts/[postId]/page.tsx`, `docs.$.tsx` to `src/app/docs/[...splat]/page.tsx`, `_app/settings.tsx` to `src/app/settings/page.tsx`. 2. Farm renders the page module's default export. Add `export default ComponentName` (name inline arrow components first), then remove `export const Route = createFileRoute(...)`. 3. Convert every `loader` plus `Route.useLoaderData()` route. Until converted it renders 500 with "useRouter must be used inside a <RouterProvider>". Use an async page for simple loaders, or a programmatic `createRoute` when the route also uses `validateSearch`, `beforeLoad`, `staleTime`, or pending/error components (see [Route options](#route-options)). 4. Interactive components work without changes: Farm detects hooks or `"use client"` and hydrates them. Adding `"use client"` to components with state or effects is still recommended. Gate: every recorded route returns its baseline status in `pnpm dev`. ### Phase 5: replace router APIs in components Search: `grep -rn "@tanstack/react-router\|@tanstack/react-start" src --include=*.tsx --include=*.ts`. Every hit outside `src/routes`, `src/router.tsx`, and `src/routeTree.gen.ts` must be converted using the [navigation](#navigation) and [route hooks](#loader-data-route-hooks-search-params-and-router-context) tables. The starter's `Header` uses `Link` with `activeProps`: ```tsx "use client"; import type { ReactNode } from "react"; import { Link } from "@farm.js/core/client"; import { usePathname } from "@farm.js/core/navigation"; function NavLink({ href, children }: { href: "/" | "/about"; children: ReactNode }) { const active = usePathname() === href; return ( <Link href={href} className={active ? "nav-link is-active" : "nav-link"}> {children} </Link> ); } ``` `usePathname()` reads the request URL during SSR, so the active class does not cause a hydration mismatch. Gate: no TanStack imports remain outside the old router files; client navigation between all routes works and component state survives where it should. ### Phase 6: server functions, server routes, middleware Convert with the [server functions](#server-functions), [server routes](#server-routes-and-api-routes), and [middleware](#middleware-and-request-utilities) tables. Gate: every former server route answers with the same status and body, and every mutation works from the UI. ### Phase 7: full verification 1. `pnpm dev`: request every route (`curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/<path>`), then open each in a browser: no console errors, interactive components respond, state behaves the same across client navigation. 2. `pnpm build`: output is written to `.farm/.output`. 3. `pnpm start`: the same route checks against the production server (`PORT` sets the port). 4. `pnpm exec tsc --noEmit`: Farm generates `src/farm.d.ts` (route, env, CSS and image types) during `farm dev`; run `pnpm exec farm generate` first when the dev server has not run. ### Phase 8: remove TanStack Only after Phase 7 passes: delete `src/routes`, `src/router.tsx`, `src/routeTree.gen.ts`, `tsr.config.json`, `.tanstack`, and remove `@tanstack/react-start`, `@tanstack/react-router`, `@tanstack/react-router-devtools`, `@tanstack/react-devtools`, `@tanstack/devtools-vite`, `@tanstack/router-cli`, `@tanstack/router-plugin`, and `@vitejs/plugin-react`. Keep `@tailwindcss/vite` when the app uses Tailwind: Farm loads it from the app. Keep `@tanstack/react-query` if the app uses it. If `tsconfig.json` lists `"vite/client"` in `types`, either keep `vite` as a devDependency on the same major as Farm (8.x) or remove that entry. Repeat Phase 7. ### Failure symptoms and recovery | Symptom | Cause | Recovery | | -------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------- | | "@vitejs/plugin-react can't detect preamble", nothing hydrates | Root `vite.config.*` still loaded | Delete it (Phase 2) | | 500 "useRouter must be used inside a <RouterProvider>" | `Route.use*`, TanStack `Link`, or router hook | Convert the route or component (Phases 4 and 5) | | Unstyled pages, no header, default title | `__root.tsx` not converted | Create `src/app/layout.tsx` (Phase 3) | | Route renders blank or 404 | No default export, or wrong folder name | Add `export default`; check the naming table | | "Duplicate page route" | Same URL from a file page and `createRoute` | Keep one of them | | Build error "createServerFn handlers run only on the server" | Client component imports a server function | Use path A from the server functions section | | `pnpm start` fails with missing module | Build not run or wrong path | Run `pnpm build`; start `node .farm/.output/server/index.mjs` | ## Feature parity reference ### Project files and tooling | TanStack | Farm | Status | | ----------------------------------------- | ---------------------------------------------------------- | ----------- | | `vite.config.ts` with `tanstackStart()` | `farm.config.ts` (`defineConfig` from `@farm.js/core`) | `different` | | `src/router.tsx` (`createRouter`) | Nothing; Farm owns the router | `different` | | `src/routeTree.gen.ts`, `tsr.config.json` | `src/farm.d.ts`, generated by `farm dev` / `farm generate` | `different` | | No start script | `node .farm/.output/server/index.mjs` | `different` | | `#/*` package.json `imports` | Unchanged | `same` | | `@/*` tsconfig path | Built-in `@/` alias to the source directory | `same` | | `defaultPreload: 'intent'` | `Link` prefetches on intent and viewport by default | `different` | | `scrollRestoration: true` | Automatic; `useScrollRestoration` for nested scrollers | `same` | Do not create a file named `src/routes.ts(x)` or `src/farm.routes.ts(x)` by accident: Farm reads those files as programmatic route definitions. The TanStack `src/routes/` directory is ignored. Docs: `/docs/project-structure`, `/docs/configuration`. ### File route naming | TanStack file (`src/routes/`) | Farm file (`src/app/`) | Status | | ------------------------------------------- | ------------------------------------------------------- | ----------- | | `index.tsx` | `page.tsx` | `rename` | | `about.tsx` | `about/page.tsx` | `rename` | | `posts.$postId.tsx` or `posts/$postId.tsx` | `posts/[postId]/page.tsx` | `rename` | | `posts.$postId.edit.tsx` | `posts/[postId]/edit/page.tsx` | `rename` | | `docs.$.tsx` (splat, `params._splat`) | `docs/[...splat]/page.tsx` (`params.splat`, `/`-joined) | `rename` | | `$.tsx` at the root (catch-all) | `[...splat]/page.tsx`, or `not-found.tsx` for 404 UI | `rename` | | `posts.tsx` with `<Outlet />` | `posts/layout.tsx` rendering `{children}` | `rename` | | `posts/route.tsx` | `posts/layout.tsx` | `rename` | | `posts/index.tsx` or `posts.index.tsx` | `posts/page.tsx` | `rename` | | `_app.tsx` + `_app/settings.tsx` (pathless) | `(app)/layout.tsx` + `(app)/settings/page.tsx` | `rename` | | `_app/settings.tsx` with no layout needed | `settings/page.tsx` | `rename` | | `(group)/page.tsx` folders | `(group)/...`, not part of the URL | `same` | | `-components/` and `-file.tsx` (ignored) | Any name: only reserved file names are routes | `rename` | | `posts_.$postId.edit.tsx` (un-nested) | Separate route groups with separate layouts | `different` | | `script[.]js.tsx` (escaped dot) | `defineRoutes` `api("/script.js", { GET })` | `different` | | `{-$lang}` optional param | No optional single param | `none` | | `__root.tsx` | `layout.tsx` at `src/app` | `different` | Reserved names in `src/app`: `page`, `layout`, `loading`, `error`, `not-found`, `route` (under `api/`), `middleware`, `default` (slots), metadata files (`sitemap`, `robots`, `manifest`, `llms`, `opengraph-image`). Every other file is ignored by routing, so colocated components need no prefix. Un-nesting: Farm layouts always wrap descendants; place the bare page in a different route group, for example `src/app/(main)/posts/layout.tsx` and `src/app/(bare)/posts/[postId]/edit/page.tsx`. Optional params: create both `src/app/page.tsx` and `src/app/[lang]/page.tsx` sharing one component, use an optional catch-all `[[...lang]]`, or use Farm `i18n` config for locale prefixes. Docs: `/docs/routing`, `/docs/layouts`. ### Layout routes and `Outlet` | TanStack | Farm | Status | | -------------------------- | ----------------------------------------------------------- | ----------- | | `<Outlet />` | `{children}` from `LayoutProps` (`@farm.js/core`) | `rename` | | Layout route `component` | `export default function Layout({ children }: LayoutProps)` | `rename` | | Layout with no index child | Add a `page.tsx` for the parent URL if it must resolve | `different` | Example: `src/app/posts/layout.tsx` exports `default function PostsLayout({ children }: LayoutProps) { return <section>{children}</section>; }`. ### Route options | `createFileRoute` option | Farm equivalent | Status | | ------------------------------- | ---------------------------------------------------------------------------------- | ----------- | | `component` | `export default` of `page.tsx` | `rename` | | `loader` | Async page (`await` in the component) or `createRoute` `data.main` | `different` | | `beforeLoad` (auth) | `middleware.ts` in the segment, `createRoute` `guard`, or `redirect()` in page | `different` | | `beforeLoad` (context) | `createRoute` `data.before`, result passed to `data.main` as `before` | `different` | | `validateSearch` | `createRoute` `search.schema`, or parse `await searchParams` in the page | `different` | | `loaderDeps` | Not needed; key data on `search` with `data.key` | `different` | | `pendingComponent` | `loading.tsx` in the segment, or `createRoute` `pending` | `rename` | | `errorComponent` | `error.tsx` (`"use client"`, props `error`, `reset`), or `createRoute` `error` | `rename` | | `notFoundComponent` | `not-found.tsx`, or `createRoute` `notFound` | `rename` | | `staleTime` | `createRoute` `data.staleTime` with `data.key` (`"30s"`, ms, or `false`) | `different` | | `gcTime`, `shouldReload` | No equivalent; invalidate with `invalidate(key)` or `revalidatePath(path)` | `none` | | `head` | `export const metadata` / `generateMetadata` (also `createRoute` options) | `different` | | `params.parse` | `createRoute` `params` schema, or `loadRouteParams` (`@farm.js/core/query/server`) | `different` | | `search.middlewares` | `createRoute` `search.preserve`, `search.stripDefaults`, `search.temporary` | `different` | | `ssr: false` / `'data-only'` | No per-route SSR switch | `none` | | `headers` | `routeRules` `headers`, or `ctx.headers.set` in middleware | `different` | | `onEnter`, `onLeave`, `onError` | No route lifecycle hooks; use effects in a client component | `none` | Simple loader, async page (verified with the starter's `posts.$postId.tsx`): ```tsx // before: src/routes/posts.$postId.tsx export const Route = createFileRoute("/posts/$postId")({ loader: ({ params }) => ({ title: `Post ${params.postId}` }), component: PostPage, }); function PostPage() { const { title } = Route.useLoaderData(); return <h1>{title}</h1>; } // after: src/app/posts/[postId]/page.tsx import type { PageProps } from "@farm.js/core"; export default async function PostPage({ params }: PageProps<"/posts/[postId]">) { const { title } = await loadPost(params.postId); // the former loader body return <h1>{title}</h1>; } ``` An async page stays server-only. `"use client"` components it renders hydrate as separate islands with serializable props; keep interactive parts in those components. Full route options, programmatic route in `src/farm.routes.tsx` (`createRoute` from `@farm.js/core`): ```tsx // before export const Route = createFileRoute("/posts/$postId")({ validateSearch: z.object({ tab: z.enum(["body", "comments"]).default("body") }), beforeLoad: ({ context }) => { if (!context.user) throw redirect({ to: "/login" }); }, loaderDeps: ({ search }) => ({ tab: search.tab }), loader: ({ params, deps }) => fetchPost(params.postId, deps.tab), staleTime: 30_000, pendingComponent: PostSkeleton, errorComponent: PostError, notFoundComponent: PostMissing, component: PostPage, }); // after: src/farm.routes.tsx (and delete src/app/posts/[postId]/page.tsx) import { createRoute, notFound, redirect } from "@farm.js/core"; import { z } from "zod"; import { PostError, PostMissing, PostPage, PostSkeleton } from "./features/posts/post-page"; export const PostRoute = createRoute("/posts/[postId]", { search: { schema: z.object({ tab: z.enum(["body", "comments"]).default("body") }) }, guard: async ({ context }) => { if (!context.user) redirect("/login"); }, data: { key: ({ params, search }) => ["post", params.postId, search.tab], staleTime: "30s", async main({ params, search }) { const post = await fetchPost(params.postId, search.tab); if (!post) notFound(); return { post }; }, }, pending: PostSkeleton, error: PostError, notFound: PostMissing, component: PostPage, }); ``` The component receives `data`, `params`, and `search` as props instead of `Route.useLoaderData()` and `Route.useSearch()`. `context` comes from `context` in `farm.config.ts` (next section). A programmatic file exports either named `createRoute` values or one default `defineRoutes(...)`; when a default export exists Farm reads only it, so list `createRoute` values inside that array. Include user, tenant, or locale in `data.key` when data depends on them. Docs: `/docs/routing` (route data cache, typed search params, route guards, route UI states), `/docs/layouts`. ### Loader data, route hooks, search params, and router context | TanStack | Farm | Status | | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------- | | `Route.useLoaderData()` | Awaited value in an async page, or `data` prop of a `createRoute` page | `different` | | `Route.useParams()` in the page | `params` prop (`PageProps<"/posts/[postId]">`) | `different` | | `useParams()` deep in a client component | Pass as props, or `useRouter({ routes: [...] }).params` (`@farm.js/core/client`) | `different` | | `Route.useSearch()` | `searchParams` prop (a Promise), `search` prop, or `useQueryState` | `different` | | `useLocation()` | `usePathname()`, `useSearchParams()` (`@farm.js/core/navigation`) | `rename` | | `useRouterState({ select: s => s.isLoading })` | `useNavigation()` (`@farm.js/core/client`): `pending`, `state`, `to` | `different` | | `useMatch`, `useMatches`, `useChildMatches` | No match tree API; use `usePathname()` or `createFarmRouter().match()` | `none` | | `Route.useRouteContext()` | Server only: `createRoute` `context`; or middleware data in `props.middleware` | `different` | | `createRootRouteWithContext` + router `context` | `context: async ({ request }) => ({ ... })` in `farm.config.ts` | `different` | | `Route.useLoaderDeps()` | No equivalent; use the `search` prop | `none` | | Search parsing in a file page | `loadSearchParams(searchParams, { q: asString.withDefault!("") })` from `@farm.js/core/query/server` | `different` | | `navigate({ search: (prev) => ({ ...prev, page: 2 }) })` | `useQueryState` / `useQueryStates` from `@farm.js/core/query/client` | `different` | | Repeated keys | `asArrayOf(parser)` | `different` | Farm route context is server-only and never serialized to the browser. Type it with `declare module "@farm.js/core" { interface FarmAppContext { ... } }`. For client-visible request values, set them in middleware with `context.data.set(key, value)` and read `props.middleware?.data.get(key)` in the page. Search params are user-editable; validate them on the server. Docs: `/docs/routing#route-context`, `/docs/query`, `/docs/middleware`. ### Navigation | TanStack (`@tanstack/react-router`) | Farm | Status | | ----------------------------------------------------- | ------------------------------------------------------------------------------------- | ----------- | | `<Link to="/about">` | `<Link href="/about">` from `@farm.js/core/client` | `rename` | | `<Link to="/posts/$postId" params={{ postId }}>` | `<Link href="/posts/[postId]" params={{ postId }}>` or ``href={`/posts/${id}`}`` | `rename` | | `<Link search={{ q }} hash="top">` | `<Link query={{ q }} hash="top">` | `rename` | | `preload="intent" \| "viewport" \| "render" \| false` | `prefetch="intent" \| "viewport" \| "render" \| "none"`, `prefetchDelay` | `rename` | | `activeProps`, `activeOptions` | Compare `usePathname()` yourself (Phase 5 example) | `none` | | `useNavigate()({ to, replace })` | `useRouter().push(href)` / `.replace(href)`, or `navigateTo(href, { replace })` | `different` | | `throw redirect({ to })` in loaders | `redirect("/path")` / `permanentRedirect()` from `@farm.js/core` (call it; it throws) | `rename` | | `<Navigate to>` component | `redirect()` on the server, or `navigateTo` in an effect | `none` | | `throw notFound()` | `notFound()` from `@farm.js/core` | `rename` | | `useRouter().invalidate()` | `useRouter().refresh()` from `@farm.js/core/navigation` | `rename` | | `router.preloadRoute({ to })` | `useRouter().prefetch(href)` (`@farm.js/core/navigation`) or `prefetch(href)` | `rename` | | `useBlocker({ shouldBlockFn })` | `useBlocker({ when, message })` from `@farm.js/core/client` | `different` | | Building hrefs | `createFarmRouter(routes).build(pattern, params, { query })` (`@farm.js/core/router`) | `different` | `Link` hrefs are type-checked against generated routes in `src/farm.d.ts`. Docs: `/docs/routing`. ### Server functions Choose one path per app. Both use `input` schemas and run the handler only on the server. | Path | When | Toolchain | Status | | --------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------- | | A. API routes as server functions | Default for every migration | `farm dev` / `farm build`, `createEndpoint` + typed `apiClient` | `different`, stable | | B. Farm server functions | Closest 1:1 shape to `createServerFn`, called from components | RSC pipeline from `@farm.js/plugin/rsc` with `experimental.serverActions: true` | `different`, experimental | | TanStack Start (`@tanstack/react-start`) | Farm | Status | | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------- | | `.inputValidator(schema)` (older `.validator`) | A: `createEndpoint({ body })` or `{ query }`; B: `createServerFn({ input })` (Zod or standard schema) | `rename` | | Handler `({ data })` | A: `({ body, query, params, request, context })`; B: `({ input, request, signal, context, error })` | `rename` | | `createServerFn({ method: "GET" })` | A: `export const GET` with a `query` schema; B: `createServerQuery` (`@farm.js/core/server-query`) | `different` | | `createServerFn({ method: "POST" })` | A: `export const POST` with a `body` schema; B: `createServerFn` (no `method` option) | `different` | | Call from a loader: `fn({ data })` | Both: `await fn(input)` from an async page or `data.main`; it runs in process | `rename` | | Call from a component, `useServerFn(fn)` | A: `apiClient.x.post({ body })`; B: `useServerFn` (`@farm.js/core/server-fn/client`) or `useAction` (`@farm.js/core/client`) | `different` | | Function `createMiddleware({ type: "function" })` | B: `createServerMiddleware` in `middleware: [...]`; A: `createEndpoint({ middleware })` | `different` | | Typed errors | `errors: { CODE: { status, data } }` and `error("CODE", data)` (A and B) | `different` | Before (TanStack): ```ts import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; export const getTodos = createServerFn({ method: "GET" }) .inputValidator(z.object({ done: z.boolean().optional() })) .handler(async ({ data }) => db.todo.findMany({ where: data })); export const addTodo = createServerFn({ method: "POST" }) .inputValidator(z.object({ title: z.string().min(1) })) .handler(async ({ data }) => db.todo.create({ data })); // loader: () => getTodos({ data: {} }) component: await addTodo({ data: { title } }) ``` After, path A (stable). Keep the handlers in a server module, mount them as API routes, call them directly on the server and through `apiClient` in the browser: ```ts // src/features/todos/server.ts import { createEndpoint } from "@farm.js/core/api"; import { z } from "zod"; export async function listTodos(input: { done?: boolean }) { return db.todo.findMany({ where: input }); } export const getTodosEndpoint = createEndpoint( { query: z.object({ done: z .enum(["true", "false"]) .transform((v) => v === "true") .optional(), }), }, async ({ query }) => ({ todos: await listTodos(query) }), ); export const addTodoEndpoint = createEndpoint( { body: z.object({ title: z.string().min(1) }) }, async ({ body }) => ({ todo: await db.todo.create({ data: body }) }), ); // src/app/api/todos/route.ts import { addTodoEndpoint, getTodosEndpoint } from "../../../features/todos/server"; export const GET = getTodosEndpoint; export const POST = addTodoEndpoint; // src/lib/api.ts import { createApiClients } from "@farm.js/core/client"; import { apiRoutes, type APIRouter } from "./api.generated"; export const { api, apiClient } = createApiClients<APIRouter>({ routes: apiRoutes }); // src/app/todos/page.tsx (server, replaces the loader) const todos = await listTodos({}); // "use client" component (replaces the component call) const result = await apiClient.todos.post({ body: { title } }); if (result.error) throw result.error; ``` Query values arrive as strings, so coerce them in the `query` schema. `src/lib/api.generated.ts` is generated by `farm dev` / `farm generate`; never import endpoint modules into `src/lib/api.ts`. On the server, `api.todos.get(...)` calls the same endpoint in process with its validation. After, path B (experimental). The declaration is the closest match to TanStack: ```ts // src/features/todos/actions.ts import { createServerFn } from "@farm.js/core/server-fn"; import { createServerQuery } from "@farm.js/core/server-query"; import { z } from "zod"; // Input omitted for brevity; a query with `input` must include every input value in `key`. export const getTodos = createServerQuery({ key: () => ["todos"], async handler() { return db.todo.findMany(); }, }); export const addTodo = createServerFn({ input: z.object({ title: z.string().min(1) }), invalidates: () => [{ key: ["todos"] }], async handler({ input }) { return db.todo.create({ data: input }); }, }); // server: const todos = await getTodos(); // client ("use client"): const add = useServerFn(addTodo); await add({ title }); // const todos = useServerQuery(getTodos, undefined); (from @farm.js/core/server-query/client) ``` Requirements for path B, verified in source: - Without the transform, a client module that imports a `createServerFn` or `createServerQuery` module fails the build with "createServerFn handlers run only on the server" (`packages/farm/src/server-query-boundary.ts`). Calls from server pages work on either path. - The transform lives in `@farm.js/plugin/rsc` (`server-fn-transform.ts`). The maintained setup, `examples/rsc-demo`, runs it from a `vite.config.ts` using `defineConfig` from `@farm.js/plugin/rsc` with `@vitejs/plugin-rsc`, `@vitejs/plugin-react`, and `react-server-dom-webpack`, and scripts `vite` / `vite build`. That is a different toolchain from the `farm dev` / `farm build` pipeline in this guide, and Phase 2 removes `vite.config.ts`. - Do not set `experimental.serverActions: true` in `farm.config.ts` as a shortcut: under `farm dev` / `farm build` the flag only disables the boundary check (`packages/farm/src/vite.ts`), so the handler module would be bundled into the browser. - Same-origin and body-size rules come from `serverActions` in config. Use path A unless the user explicitly accepts the experimental RSC toolchain. Docs: `/docs/api-routes`, `/docs/api-client`, `/docs/server-queries`, `/docs/routing#route-actions`, `/docs/configuration#server-action-security`. ### Server routes and API routes | TanStack | Farm | Status | | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------- | | `createFileRoute("/api/x")({ server: { handlers: { GET } } })` | `src/app/api/x/route.ts` exporting `GET` | `different` | | `createServerFileRoute().methods({...})`, `createAPIFileRoute` | Same: `src/app/api/**/route.ts` | `different` | | Handler `({ request, params })` | Plain export receives `request`; `createEndpoint` handler receives `{ request, params, body, query, headers, context }` | `different` | | Server route outside `/api` | `defineRoutes(({ api }) => [api("/feed.xml", { GET })])` in `src/farm.routes.ts` | `different` | | Server route and page on the same path | API handlers live under `/api` (or a programmatic `api()` path) | `none` | | Per-route handler middleware | `createEndpoint({ middleware: [fn] }, handler)` | `different` | ```ts // before: src/routes/api/hello.ts export const Route = createFileRoute("/api/hello")({ server: { handlers: { GET: async ({ request }) => Response.json({ ok: true }) } }, }); // after: src/app/api/hello/route.ts export const GET = async () => Response.json({ ok: true }); ``` A route without a `HEAD` export answers `HEAD` with its `GET` handler. Change the `/api` mount with `api: { basePath }` in `farm.config.ts`. Docs: `/docs/api-routes`, `/docs/routing#programmatic-routes`. ### Middleware and request utilities | TanStack | Farm | Status | | ------------------------------------------------------------ | --------------------------------------------------------------------------------------- | ----------- | | Global `requestMiddleware` in `src/start.ts` (`createStart`) | `src/app/middleware.ts`, or `middleware: [{ matcher, handler }]` in `farm.config.ts` | `different` | | Request `createMiddleware().server(({ next }) => next())` | `middleware().use(async (ctx, next) => { await next(); })` (`@farm.js/core/middleware`) | `rename` | | Global `functionMiddleware` | Add `createServerMiddleware` to each `createServerFn` `middleware` array | `different` | | Client half `.client(...)` of function middleware | No client-side middleware for server functions | `none` | | `getRequest()` | `getCurrentRequest()` from `@farm.js/core/request` | `rename` | | `getRequestHeader(s)`, `getCookie` | `headers()`, `cookies()` from `@farm.js/core/headers` (read-only) | `rename` | | `setCookie`, `setResponseHeader` | `ctx.cookies.set`, `ctx.headers.set` in middleware; headers on a `Response` | `different` | A global request middleware becomes `src/app/middleware.ts` exporting `middleware().use(async (ctx, next) => { ctx.headers.set("x-app", "farm"); await next(); })`, or a named `export async function middleware(request, context)` that continues unless it returns a `Response`. Returning a `Response` stops the chain. Segment `middleware.ts` files run from the root down for pages and API routes in that segment. Docs: `/docs/middleware`. ### TanStack Query | TanStack | Farm | Status | | ---------------------------------------------------------- | ----------------------------------------------------------------------- | ----------- | | `@tanstack/react-query` hooks | Keep the package; provide `QueryClientProvider` from a client component | `same` | | `QueryClient` in router context | Create it in the provider with `useState` | `different` | | `setupRouterSsrQueryIntegration` / `routerWithQueryClient` | No router integration; pass server data as `initialData` props | `none` | | `ensureQueryData` in loaders | Fetch in an async page or `data.main`, pass the result as props | `different` | | Farm-native alternative | `createServerQuery` + `useServerQuery`, or `apiClient` cache options | `different` | ```tsx // src/components/providers.tsx "use client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useState, type ReactNode } from "react"; export function Providers({ children }: { children: ReactNode }) { const [client] = useState(() => new QueryClient()); return <QueryClientProvider client={client}>{children}</QueryClientProvider>; } ``` Render `<Providers>` around `{children}` in `src/app/layout.tsx`. Components rendered by an async page hydrate as separate islands, so they cannot read React context from a layout provider; keep pages whose components call `useQuery` synchronous, or wrap the island in its own provider. `@tanstack/react-query-devtools` can stay inside `Providers`. Docs: `/docs/server-queries`, `/docs/api-client`. ### Rendering: SSR, streaming, deferred data, prerendering | TanStack | Farm | Status | | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | ----------- | | SSR by default | SSR by default | `same` | | Unawaited loader promise + `<Await>` / `defer()` | `defer()` from `@farm.js/core` in `createRoute` `data.main`, React `use()` inside `<Suspense>` | `different` | | `<Suspense>` boundaries | Same, plus `loading.tsx` as the segment fallback | `same` | | Selective SSR (`ssr: false`, `'data-only'`) | No switch; render browser-only parts in a client component after mount | `none` | | `<ClientOnly>`, `useHydrated` | Mounted-state wrapper with `useEffect`; `createClientOnlyFn` for functions | `none` | | SPA mode (`spa: { enabled: true }`) | No SPA shell mode | `none` | | `prerender: { enabled: true }` | `export const dynamic = "force-static"` or `"use ssg"` per page; `routeRules: { "/": { prerender: true } }` | `different` | | `prerender.crawlLinks` | `generateStaticParams` for dynamic segments; no link crawling | `different` | | ISR via headers | `export const revalidate = 60` or `routeRules` `swr` / `isr` | `different` | With `defer()`, `data.main` returns `{ product, reviews: defer(getReviews(id)) }`; the component types the field as `Deferred<Review[]>` (from `@farm.js/core`) and resolves it with `use(reviews)` inside `<Suspense>`. Keep data needed for the title, redirects, or authorization awaited, not deferred. Docs: `/docs/server-rendering`, `/docs/routing#deferred-route-data`, `/docs/cache-ppr`. ### Head and SEO | TanStack `head()` / root `head` | Farm | Status | | ---------------------------------------------------- | ----------------------------------------------------------------- | ----------- | | `meta: [{ title }]`, `{ name: "description" }` | `export const metadata: Metadata = { title, description }` | `different` | | Title from loader data | `export async function generateMetadata({ params })` | `different` | | Title templates | `title: { default, template: "%s \| Site" }` in a layout | `different` | | `links: [{ rel: "icon" }]` | `metadata.icons`; other `<link>` tags in the root layout `<head>` | `different` | | `links: [{ rel: "stylesheet", href: css }]` (`?url`) | `import "./styles.css"` in the layout | `different` | | `scripts: [...]` | `<script>` in the root layout `<head>` or `<body>` | `different` | | `<HeadContent />`, `<Scripts />` | Remove; Farm injects head tags and scripts | `none` | | Server route for `sitemap.xml` / `robots.txt` | `src/app/sitemap.ts`, `src/app/robots.ts` | `different` | `Metadata` and `MetadataProps` are types from `@farm.js/core`. Docs: `/docs/routing#metadata-and-og-images`. ### Environment variables and environment functions | TanStack | Farm | Status | | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ----------- | | `import.meta.env.VITE_*` in client code | Unchanged (Farm keeps Vite's `VITE_` prefix and `.env` loading) | `same` | | `process.env.X` on the server | Unchanged | `same` | | Typed env | Optional `env: { server: {...}, public: {...} }` in `farm.config.ts`, read with `getEnv` / `getPublicEnv` from `@farm.js/core/env` | `different` | | `createServerOnlyFn(fn)` | `createServerOnlyFn(fn)` from `@farm.js/core/environment` | `rename` | | `createClientOnlyFn(fn)` | `createClientOnlyFn(fn)` from `@farm.js/core/environment` | `rename` | | `createIsomorphicFn().server(a).client(b)` | `createIsomorphicFn({ server: a, client: b })` (inline object) | `different` | Never expose secrets through `VITE_` or `public`. Docs: `/docs/environment-functions`, `/docs/deployment#environment-variables`. ### Styling and devtools - Tailwind v4 (`different`): remove `tailwindcss()` from the Vite config; Farm enables it in dev and build unless the project has a PostCSS config. `@import "tailwindcss"`, `@theme`, and `@plugin` stay (`same`); keep `tailwindcss`, `@tailwindcss/vite`, and plugins such as `@tailwindcss/typography` installed. - `styles.css?url` plus a head link (`different`): side-effect `import "../styles.css"` in the layout. - `@tanstack/react-devtools`, `@tanstack/devtools-vite` (`different`): add `@farm.js/devtools@<version>` as a devDependency and `plugins: [devtools()]` (`devtools` from `@farm.js/devtools`). - `@tanstack/react-router-devtools` (`different`): the routes view in Farm DevTools, or `farm explain <path>`. `@tanstack/react-query-devtools` (`same`): keep it inside `Providers`. Docs: `/docs/plugins/devtools`. ### Deployment | TanStack | Farm | Status | | ------------------------------------------------ | --------------------------------------------------------------------- | ----------- | | Nitro or hosting Vite plugin in `vite.config.ts` | `deploy: { target: "vercel" \| "cloudflare" \| "netlify" \| "node" }` | `different` | | Nitro preset name | `deploy: { preset: "aws-lambda" }` or `farm build --preset <name>` | `different` | | `.output/server/index.mjs` | `.farm/.output/server/index.mjs` (node target default) | `rename` | | Health checks | `GET /_farm/health/live`, `GET /_farm/health/ready` (Node output) | `different` | A preset overrides a target. Do not add `nitro` or `h3` overrides; Farm pins them. For Docker, install devDependencies for `farm build`; the runtime image only needs `.farm/.output`. Docs: `/docs/deployment`, `/docs/route-runtime`. ## Final checklist - [ ] `vite.config.*` removed from the project root; needed plugins moved to `farm.config.ts` `vite.plugins`. - [ ] `src/app/layout.tsx` renders the document, imports global CSS, and exports `metadata`. - [ ] Every route in the baseline list has a `page.tsx` (or `createRoute`) with a default export. - [ ] No `@tanstack/react-router` or `@tanstack/react-start` imports remain in `src` outside deleted files. - [ ] Server functions are called from server code or through API routes; secrets stay server-only. - [ ] `pnpm dev`, `pnpm build`, `pnpm start`, and `pnpm exec tsc --noEmit` pass. - [ ] Each route returns its baseline status in dev and production; hydration and client navigation work. - [ ] Farm packages pinned to exact versions; TanStack packages and generated files removed last. --- ## Observability and tracing URL: /docs/observability Export OpenTelemetry traces and listen to correlated Farm runtime events in development and production. # Observability and tracing Farm has three complementary observability layers: - OpenTelemetry traces show the request timeline and export to any OTLP-compatible backend. - OpenTelemetry metrics cover Farm's own cache, PPR, and streaming outcomes. - Farm events expose detailed framework lifecycle data for logs, alerts, and custom integrations. The same lifecycle events are attached to the active OpenTelemetry span, and delivered events include `traceId`, `spanId`, and `traceSampled` for correlation. ## OpenTelemetry quick start Install Farm's optional Node SDK setup package: ```bash pnpm add @farm.js/otel ``` Create one instrumentation file at `src/instrumentation.ts` or `instrumentation.ts`: ```ts import type { FarmInstrumentationContext } from "@farm.js/core/instrumentation"; export async function register(context: FarmInstrumentationContext) { if (context.runtime !== "nodejs") return; const { registerOTel } = await import("@farm.js/otel"); return registerOTel({ serviceName: "storefront", serviceVersion: process.env.APP_VERSION, }); } ``` Enable Farm spans in `farm.config.ts`: ```ts import { defineConfig } from "@farm.js/core"; export default defineConfig({ observability: { tracing: { attributes: { "deployment.environment": process.env.NODE_ENV ?? "development", }, }, }, }); ``` `@farm.js/otel` uses the OTLP HTTP trace exporter by default. Configure the destination with standard OpenTelemetry environment variables: ```bash OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector.example.com OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer%20TOKEN" ``` Use `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` when traces need a different endpoint. You can also pass a custom `traceExporter`, `spanProcessors`, sampler, resource, instrumentation list, or `metricReaders` to `registerOTel`. Both pieces are intentional: `instrumentation.ts` starts an SDK/exporter, while `observability.tracing` tells Farm to create framework spans. Without an SDK, the OpenTelemetry API remains a no-op. ## What Farm traces | Span | Meaning | | ----------------------------------------- | ----------------------------------------------------------------------------- | | `GET /products/:id` | Server request span with method, route, path, status, and propagated context. | | `farm.render /products/:id` | Completed page render. | | `farm.middleware auth` | Completed middleware execution. | | `farm.api POST /api/products` | Completed API route execution. | | `farm.integration stripe.checkout.create` | Completed integration operation when that event is emitted. | | `farm.storage query` | Completed integration database operation when that event is emitted. | | `farm.ppr.refresh /dashboard` | Completed PPR refresh. | | `farm.plugin plugin.hook` | Completed plugin hook when that event is emitted. | Incoming W3C `traceparent` headers are extracted automatically. A matched Farm route renames the request span from the raw pathname to the route pattern and sets `http.route`. Status codes of 500 or higher and thrown errors mark spans as errors. To wrap application work in a typed Farm-aware child span: ```ts import { runWithFarmSpan } from "@farm.js/core/observability"; const result = await runWithFarmSpan( "catalog.recommendations", () => loadRecommendations(productId), { kind: "integration", attributes: { "product.id": productId }, }, ); ``` Use `getFarmTraceContext()` when a log or provider call needs the current trace and span IDs. ## What Farm measures `@farm.js/otel` also subscribes to Farm's event bus and records the framework's own cache, PPR, streaming, and middleware outcomes. Auto-instrumentation only sees generic HTTP, filesystem, and database work, so this is the layer that makes cache hit rate, tag invalidation, and shell TTFB visible. | Instrument | Kind | Attributes | | ----------------------------------- | --------------- | --------------------------------------------------------------------------------------------------- | | `farm.cache.lookups` | Counter | `farm.cache.result` (`hit`, `miss`, `stale`, `dedupe`, `bypass`), `farm.cache.stale` on a stale hit | | `farm.cache.invalidations` | Counter | `farm.cache.operation` (`revalidateTag`, `updateTag`, `revalidatePath`, `invalidated`) | | `farm.cache.invalidated_entries` | Counter | `farm.cache.operation`, incremented by the number of entries the operation removed | | `farm.ppr.shell.lookups` | Counter | `farm.ppr.result` (`hit`, `miss`, `cached`, `bypass`, `invalidated`) | | `farm.ppr.refresh.duration` | Histogram, `ms` | none | | `farm.render.stream.shell.duration` | Histogram, `ms` | none, this is shell TTFB | | `farm.middleware.short_circuits` | Counter | `farm.route`, the configured matcher rather than the request path, and `farm.http.status_class` | Metric attributes are deliberately bounded. A cache `key` embeds the serialized arguments of the cached call, a tag is authored at runtime, and the `route` on cache, PPR, and streaming events is the request pathname rather than a route pattern, so none of them becomes a metric attribute. That detail is recorded as an event on the active span instead, where per-request cardinality is free and a stale serve stays attributable to the request that caused it. When `observability.tracing` is enabled Farm already writes those span events itself, so `@farm.js/otel` does not add a second copy. Metrics need a metric reader, and Farm registers one only when you ask for it. Name an exporter with `OTEL_METRICS_EXPORTER`, or pass `metricReaders` to `registerOTel`: ```bash OTEL_METRICS_EXPORTER=otlp # otlp, console, prometheus, or none ``` Without it there is no meter provider, the instruments are no-ops, and the span events still work. That opt-in is deliberate: left to its own defaults the Node SDK exports metrics to `localhost:4318` whether or not an app configured metrics, and with nothing listening there every recorded metric turns graceful shutdown into the exporter's retry window, around eight seconds. Traces are unaffected and keep using the OTLP trace endpoint. Pass `farmEvents` to narrow or disable the mapping: ```ts return registerOTel({ serviceName: "storefront", // Leave Farm's event bus unsubscribed. farmEvents: false, // Or keep one half of it. // farmEvents: { metrics: true, spanEvents: false }, }); ``` An app that starts its own SDK can map the bus without `registerOTel`. Call it after the SDK has started, because an instrument created before a meter provider is registered stays a no-op, and dispose it on shutdown so a development restart does not stack subscribers. ```ts import { recordFarmEvents } from "@farm.js/otel"; const disposeFarmEvents = await recordFarmEvents(); ``` ## Instrumentation lifecycle Farm calls `register(context)` once before the development server or production request runtime starts. The context contains: - `mode`: `development`, `production`, or `test` - `runtime`: `nodejs`, `edge`, or `bun` - `root`: the runtime working directory `register` may return a cleanup function or an object with `shutdown()`. An exported `shutdown()` function is also supported. Farm runs cleanup during development server close and production graceful shutdown, after active requests drain, so batched spans are flushed before the process exits. Keep Node-only SDK imports inside the `runtime === "nodejs"` branch. Edge and Bun deployments can initialize a runtime-compatible OpenTelemetry SDK in their own branch without changing Farm's tracing API. ## Subscribe to events **farm.config.ts** ```ts import { onFarmEvent } from "@farm.js/core/observability"; onFarmEvent((event) => { if (event.level === "error") { console.error("[farm]", event.type, event); } }); ``` ## Event families | Family | Examples | | -------------------- | -------------------------------------------------------------------------------- | | Request | request.start, request.complete, request.error | | Server | server.start, server.ready, server.shutdown | | Routing | route.discovered, route.matched, route.notFound, route.redirect | | Rendering | render.start, render.complete, render.stream.shellReady, render.error | | Cache | cache.hit, cache.miss, cache.set, cache.revalidateTag | | PPR | ppr.shell.hit, ppr.shell.cached, ppr.shell.invalidated | | Middleware | middleware.start, middleware.complete, middleware.shortCircuit, middleware.error | | Integrations | integration.ready, integration.api.call.start, integration.webhook.verified | | MCP | mcp.tool.complete (registered callback outcome, not an agent run) | | Agents | agent.read (Markdown mirror, llms.txt, or API catalog read) | | Integration database | storage.query.start, storage.schema.ready | | Build | build.start, routes.generated, types.generated, manifest.generated | The `storage.query.*` and `storage.schema.*` event names are compatibility identifiers for integration database activity. They do not describe `getStorage()` KV operations. ## Configure in farm.config.ts Use config-level observability when an app should log or forward events from startup. ```ts import { defineConfig } from "@farm.js/core"; export default defineConfig({ observability: { logs: true, tracing: true, events: ["cache.hit", "cache.miss", "ppr.shell.cached"], onEvent(event) { if (event.type === "cache.miss") { console.log("Cache miss", event.key, event.reason); } }, }, }); ``` `events` is optional. Leave it out to receive every emitted event. Filtering event delivery does not disable span creation, the events recorded on active spans, or the `@farm.js/otel` metrics; use `farmEvents: false` for those. ## Runtime subscription Use `onFarmEvent` for tests, local debugging, or integration packages that want to register listeners without owning app config. ```ts import { onFarmEvent } from "@farm.js/core/observability"; const unsubscribe = onFarmEvent((event) => { if (event.type === "integration.webhook.failed") { console.error(event.integration, event.reason); } }); unsubscribe(); ``` ## Useful event types | Area | Events | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cache | `cache.hit`, `cache.miss`, `cache.set`, `cache.stale`, `cache.bypass`, `cache.invalidated`, `cache.revalidatePath`, `cache.revalidateTag`, `cache.updateTag`, `cache.error` | | PPR | `ppr.shell.hit`, `ppr.shell.miss`, `ppr.shell.cached`, `ppr.shell.bypass`, `ppr.shell.invalidated`, `ppr.suspense.holeDetected`, `ppr.refresh.start`, `ppr.refresh.complete`, `ppr.refresh.error` | | API | `api.request.start`, `api.request.complete`, `api.validation.failed`, `api.error` | | MCP | `mcp.tool.complete` — configured server, transport route, tool name, outcome and duration; no tool payloads | | Agents | `agent.read` — surface (`markdown`, `llms`, `llms-full`, `api-catalog`), path, Markdown view (`outline`, `section`, `parts`, `actions`), status, and the agent the User-Agent declares | | Integrations | `integration.registered`, `integration.config.validated`, `integration.ready`, `integration.disposed`, `integration.api.call.start`, `integration.api.call.complete`, `integration.webhook.verified`, `integration.webhook.failed` | | Middleware | `middleware.start`, `middleware.complete`, `middleware.shortCircuit`, `middleware.error` | | Integration database | `storage.query.start`, `storage.query.complete`, `storage.query.error`, `storage.schema.ready`, `storage.schema.error` | Middleware events include the matched middleware route, request pathname, middleware file/config name, duration for completes, status for short circuits, and the thrown error for failures. MCP completion events distinguish tool errors from successful HTTP transport responses. They cover registered callbacks only, excluding authorization and SDK rejections before callback execution. See [MCP tool observability](/docs/plugins/mcp#observe-tool-executions) for payload and coverage details. ## Agent reads `agent.read` fires each time an agent surface answers: a Markdown mirror or `page.md` (including `?outline`, `?section`, `?max_tokens` parts, and `?actions`), `/llms.txt` or `/llms-full.txt`, and `/.well-known/api-catalog`. It carries the path served, never the query string, the status, and the agent the request's User-Agent declares: ```ts onFarmEvent((event) => { if (event.type !== "agent.read") return; // { surface: "markdown", route: "/pricing.md", view: "parts", status: 200, // agent: { family: "openai", category: "agent", identity: "self-reported" } } metrics.increment("agent_reads", { surface: event.surface, family: event.agent.family }); }); ``` `agent.category` is `agent` for user-triggered fetchers such as ChatGPT-User, Claude-User, and Perplexity-User, and `crawler` for indexing and training crawlers such as GPTBot, ClaudeBot, and PerplexityBot. The identity is `self-reported`: any client can send these tokens, so use it to understand traffic, never to grant access. Farm's own reads, such as llms-full.txt reading each page, are not reported. `classifyFarmAgent(userAgent)` from `@farm.js/core` returns the same identity for any request, for example in middleware. ## Middleware event payloads Middleware events are emitted for both `farm.config.ts` middleware entries and `src/app/**/middleware.ts` files in development and production. | Event | Payload | | ------------------------- | ----------------------------------------- | | `middleware.start` | `route`, `pathname`, `name` | | `middleware.complete` | `route`, `pathname`, `name`, `durationMs` | | `middleware.shortCircuit` | `route`, `pathname`, `name`, `status` | | `middleware.error` | `route`, `pathname`, `name`, `error` | ```ts import { defineConfig } from "@farm.js/core"; export default defineConfig({ observability: { onEvent(event) { if (event.type === "middleware.shortCircuit") { console.log(event.pathname, event.status); } }, }, }); ``` ## Production notes - Keep `@farm.js/otel` in `dependencies`, not `devDependencies`, so it is available in the deployed server bundle. - Set `OTEL_SERVICE_NAME` or pass `serviceName` explicitly; use deployment/version resource attributes for release comparisons. - Farm's production lifecycle waits for active requests and then shuts down instrumentation, allowing the batch processor to flush. - `forceFlushOTel()` flushes spans and, when a metrics pipeline is configured, recorded metrics. Call it from `after()` on hosts that freeze the process between requests, where the periodic metric reader never gets to export. - Filter high-volume events before shipping them to a log drain. - Use the emitted trace and span IDs to correlate Farm events with application logs. - Treat event payloads as operational metadata; do not put secrets in event fields. - Watch `api.validation.failed`, `integration.webhook.failed`, `cache.error`, and `ppr.refresh.error` in production. --- ## OpenAPI Reference URL: /docs/openapi Generate and publish API reference docs from Farm API route metadata, with Scalar-style presentation. # OpenAPI Reference Generate and publish API reference docs from Farm API route metadata, with Scalar-style presentation. ## Enable OpenAPI **farm.config.ts** ```ts export default defineConfig({ openapi: { enabled: true, route: "/docs/reference", title: "Farm API", version: "1.0.0", }, }); ``` ## Reference route The OpenAPI route can be included in generated route types so docs navigation and Link hrefs stay aware of the reference page. Use an absolute pathname, such as `/docs/reference`. Farm rejects query strings, fragments, backslashes, encoded separators, and dot segments such as `..` or `%2e%2e` that browsers would reinterpret. Integration route paths and `workflows.route` use the same validation. ## What gets documented Farm scans API route files and generates an OpenAPI 3.0.3 spec from the discovered routes, methods, route params, query schemas, body schemas, and response metadata it can infer. **src/app/api/users/route.ts** ```ts import { createEndpoint } from "@farm.js/core/api"; import { z } from "zod"; export const GET = createEndpoint( { method: "GET", query: z.object({ limit: z.coerce.number().int().positive().default(20), }), }, async ({ query }) => { return Response.json({ users: await listUsers(query.limit), }); }, ); ``` The route appears in the generated reference with a typed `limit` query parameter. Dynamic API segments use OpenAPI path parameters. Optional catch-all routes such as `/api/files/[[...slug]]` produce both `/files` and `/files/{slug}` operations because OpenAPI path parameters themselves must always be required. Required catch-all routes produce only the parameterized operation. `QUERY` routes include their request-body schema too. Farm currently emits OpenAPI 3.0.3, whose Path Item Object does not have a native `query` field. To keep the document valid, Farm places each `QUERY` Operation Object under the registered `x-oai-additionalOperations.QUERY` extension. This maps directly to the native `query` field available in OpenAPI 3.2 without mislabeling the route as `GET` or `POST`. ## Add metadata ```ts export default defineConfig({ openapi: { enabled: true, route: "/docs/reference", title: "Acme API", description: "Public and internal API routes for Acme.", version: "1.0.0", servers: [ { url: "https://api.acme.com", description: "Production", }, ], contact: { name: "API Support", email: "support@acme.com", }, }, }); ``` ## Serve the reference The configured route serves a Scalar-powered reference page with the generated spec embedded in the page. The route can be visited during development and included in production docs. Farm pins the Scalar browser assets to the reviewed version shipped with the framework and verifies CDN responses with subresource integrity. Scalar upgrades therefore arrive with Farm releases instead of floating independently in production. ## Choose the API server URL Set `openapi.servers` when the document must advertise an explicit public server. Otherwise Farm uses an absolute `api.baseURL` when configured. During development, a root-relative API base is joined to the current request origin (or `http://localhost:3000` when generating without a request). Production documents keep a root-relative API base relative, so deployed clients and the interactive reference stay on the deployment's current origin instead of pointing at localhost. The precedence is `openapi.servers`, an absolute Farm `api.baseURL`, the current development request origin plus Farm's API path, then the mode-specific default. ## Describe authentication OpenAPI operations are public by default because Farm cannot infer authorization from arbitrary middleware. Set a document-wide default with `openapi.security`, then override individual typed endpoints when public and protected routes share one document: ```ts export default defineConfig({ openapi: { enabled: true, security: "cookie" }, }); export const GET = createEndpoint({ method: "GET", openapi: { security: "public" } }, async () => ({ status: "ok", })); export const POST = createEndpoint( { method: "POST", openapi: { security: "bearer" } }, async () => ({ created: true }), ); ``` Supported modes are `public`, `bearer`, `cookie`, and `either`. This metadata documents the contract; authentication middleware must still enforce it at runtime. ## Describe responses Farm does not guess status codes or body formats from arbitrary handler code. Add response metadata to a typed endpoint when clients need an exact contract: ```ts export const GET = createEndpoint( { method: "GET", openapi: { responses: { 200: { body: "json", description: "Current account", schema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], }, }, 404: { body: "empty", description: "Account not found" }, }, }, }, async () => Response.json(await getAccount()), ); ``` Use `body: "empty"` for responses without content, `body: "stream"` with an explicit `contentType` for streams, and `body: "binary"` for files. JSON and binary responses default to `application/json` and `application/octet-stream`. When response metadata is absent, Farm emits an unspecified default response instead of claiming an unknown status code or JSON schema. **Terminal** ```bash farm generate farm build ``` `farm dev` and `farm generate` keep route types aware of the reference URL. `farm build` includes the reference route in the app output when OpenAPI is enabled. ## Production notes - Enable OpenAPI for APIs you want to document publicly or internally. - Keep private admin routes out of public docs unless the site is protected. - Use Zod schemas on API routes for better generated parameter and body details. - Farm recognizes Zod schemas structurally, including Zod 3/4 schemas loaded from another package copy. Standard Schema validators without portable schema metadata remain valid at runtime, but the generated document uses an explicit metadata-unavailable fallback instead of inventing a shape. - Pair OpenAPI with typed callers so server/client code and published docs describe the same route surface. --- ## Farm.js Documentation URL: /docs The complete Farm.js framework guide, powered by @farming-labs/docs. # Farm.js Documentation Farm.js docs are served from the Farm docs runtime. The same markdown files power human pages, markdown routes, llms.txt, sitemap output, search, and agent discovery through `/api/docs`. ### Start Create an app and learn the files that matter. - [Getting Started](/docs/getting-started): Create a Farm.js app, understand the files that matter, and run the development server. - [Project Structure](/docs/project-structure): The compact file layout Farm expects, plus the optional files you add only when the app needs them. - [Configuration](/docs/configuration): Use farm.config.ts as the single project control plane for source paths, integrations, docs, KV storage, database clients, deployment, and framework behavior. - [Renderers](/docs/renderers): Choose React, Preact, Solid, Vue, or Svelte and review the cross-renderer feature-support matrix. ### Core Routes, rendering, layouts, and request flow. - [Routing](/docs/routing): Farm uses an app directory routing model with static routes, dynamic segments, catch-all routes, and typed navigation. - [Fonts](/docs/fonts): Compile local or remote fonts into self-hosted, hashed assets with generated CSS and no loader runtime. - [Themes](/docs/themes): Configure light, dark, and system color modes with a pre-paint selector, Tailwind variants, and typed client and server APIs. - [Layouts and Route Boundaries](/docs/layouts): Wrap routes with root and nested layouts, then use loading, error, and not-found files for route-level UX. - [Rendering Model](/docs/server-rendering): Choose dynamic rendering, static rendering, ISR, or PPR with route-level exports and config. - [React Renderer](/docs/renderers/react): Use the default renderer with the complete FARMJS client API, integrations, and experimental Server Components. - [Preact Renderer](/docs/renderers/preact): Use Preact hooks, streaming SSR, hydration, and React-compatible FARMJS client APIs. - [Solid Renderer](/docs/renderers/solid): Use Solid signals, SSR, hydration, and renderer-neutral FARMJS server primitives. - [Vue Renderer](/docs/renderers/vue): Use Vue SFCs, SSR, hydration, and typed FARMJS server calls. - [Svelte Renderer](/docs/renderers/svelte): Use Svelte 5 components, runes, SSR, hydration, and typed FARMJS server calls. - [Built-in Authentication](/docs/auth): Enable email/password auth, sessions, server helpers, and React APIs with the top-level `auth` framework config. - [Middleware](/docs/middleware): Run request behavior before routes, pass request-scoped data to pages, and short-circuit with redirects or responses. - [Environment Functions](/docs/environment-functions): Remove server-only or client-only implementations from the opposite bundle while preserving one typed interface. ### Data and APIs Typed params, API routes, API callers, KV storage, and database access. - [Query and Params](/docs/query): Parse search params and route params with typed helpers on the server and synchronized state on the client. - [API Routes](/docs/api-routes): Expose HTTP handlers from src/app/api and validate input with schemas before handler code runs. - [API Client](/docs/api-client): Define one shared `{ api, apiClient }` pair for typed local server calls and HTTP calls, with cache policies, invalidation, retries, callbacks, and optimistic updates. - [Server Queries](/docs/server-queries): Define typed server reads once, deduplicate requests, prefetch browser data, use SWR, and share invalidation keys with routes and APIs. - [KV Storage](/docs/storage): Use `getStorage()` for caches, settings, counters, idempotency records, and object-backed values. ### Integrations Provider integrations and custom integration contracts. - [Integrations](/docs/integrations): Register services once, get owned routes, typed callers, providers, middleware, database models, lifecycle hooks, and validation. - [Stripe Integration](/docs/integrations/stripe): Add checkout, portal sessions, billing status, webhooks, product catalogs, metering, and database-backed billing snapshots. - [Autumn Integration](/docs/integrations/autumn): Add subscription, entitlement, and usage billing with schema-backed database records through the integration ORM. - [Polar Integration](/docs/integrations/polar): Use Polar for products, checkout, customer portals, subscriptions, webhooks, and entitlement-aware app flows. - [Auth Integrations](/docs/integrations/auth): Choose built-in Farm Auth or an explicit Better Auth, Auth.js, Clerk, Auth0, WorkOS, or Supabase integration. - [Email Integration](/docs/integrations/email): Render React Email templates, send with Resend, schedule messages, preview templates, and receive webhooks. - [Jobs Integration](/docs/integrations/jobs): Define typed tasks once and run them through Trigger.dev or Inngest with trigger, schedule, batch, status, and cancel APIs. - [Unkey Integration](/docs/integrations/unkey): Create, verify, revoke, update, and delete API keys, plus protect routes with key verification and rate-limit checks. - [UI Registry](/docs/integrations/ui-registry): Opt into shadcn-style UI scaffolds for built-in integrations when you want working screens with the integration setup. - [Database and ORM Clients](/docs/integrations/orm-storage): Use application-owned ORM models, schema-backed integration databases, and adapter-owned data without confusing them with KV storage. ### Runtime Cache, PPR, observability, instant preview, and deployment output. - [Post-response Work](/docs/after): Schedule short server work with after() without delaying the response. - [Cache and PPR](/docs/cache-ppr): Use shared runtime cache helpers, tag/path invalidation, ISR-style revalidation, and static shell caching for PPR pages. - [Observability](/docs/observability): Export OpenTelemetry traces and consume correlated Farm runtime events in development and production. - [Built-in DevTools](/docs/devtools): Inspect the resolved app runtime in the browser during development. - [Doctor](/docs/doctor): Run the same runtime diagnostics from the terminal or CI, against a live app or offline. - [Cron](/docs/cron): Map portable UTC schedules to ordinary API routes, run them locally, and compile them to deployment-native triggers. - [Instant Preview](/docs/preview): Expose the current local app through a public URL for sharing, webhooks, OAuth callbacks, and external testing. - [Deployment](/docs/deployment): Build deployable output with Farm's deploy config and Nitro presets, from first-class targets to custom Nitro output. ### Content Docs runtime, markdown mirrors, and OpenAPI. - [Docs Engine](/docs/docs-engine): Serve a @farming-labs/docs-inspired docs runtime from Farm config, including human pages and agent-readable API routes. - [Markdown Mirrors](/docs/markdown): Expose markdown versions of app pages so agents, crawlers, docs tools, and support workflows can read rendered content as text. - [OpenAPI Reference](/docs/openapi): Generate and publish API reference docs from Farm API route metadata, with Scalar-style presentation. ### Plugin ecosystem Plugin system and lifecycle hooks. - [Plugin Ecosystem](/docs/plugins): Extend server and browser behavior across config, requests, routing, rendering, hydration, navigation, builds, and HMR. - [API MCP Plugin](/docs/plugins/mcp): Expose opted-in typed API routes as authenticated MCP tools over Streamable HTTP. - [Hints Plugin](/docs/plugins/hints): Find accessibility, Web Vitals, HTML, and third-party script problems on the route you are developing. - [Content Plugin](/docs/plugins/content): Validate local Markdown, MDX, JSON, and YAML as typed server collections without a second config file. - [Federation Plugin](/docs/plugins/federation): Publish and load independently deployed browser modules while Farm keeps server code and credentials isolated. - [Health Plugin](/docs/plugins/health): Expose dependency-aware liveness, readiness, and startup probes to hosting platforms. - [Partytown Plugin](/docs/plugins/partytown): Move explicitly opted-in analytics and tag-manager scripts into a web worker with typed callers. - [PWA Plugin](/docs/plugins/pwa): Generate a route-aware service worker with offline navigation, update prompts, and concise SWR image caching. - [Search Plugin](/docs/plugins/search): Build a chunked index from static pages and query it through a typed, renderer-neutral browser client. - [Scripts Plugin](/docs/plugins/scripts): Load external browser SDKs with typed handles, deliberate timing, consent gates, dependencies, and retries. - [Sentry Plugin](/docs/plugins/sentry): Report server errors with route context and trace Farm requests with `@sentry/node`. - [StyleX Plugin](/docs/plugins/stylex): Compile StyleX with pre-paint development styles, CSS HMR, and extracted production CSS. - [WebAssembly Plugin](/docs/plugins/wasm): Import prebuilt Wasm modules in browser code and module workers. - [WebMCP Plugin](/docs/plugins/webmcp): Expose a small, explicit, typed browser tool surface to agents using the experimental WebMCP API. - [Create a Plugin](/docs/plugins/create-plugin): Build a plugin with definePlugin when app behavior belongs in reusable framework lifecycle hooks. - [Client Plugin API](/docs/plugins/client): Attach typed hydration, navigation, error, performance, and cleanup hooks to a plugin through a browser-safe module. ### Migrations Move an existing framework app to Farm with inspectable, dry-run-first migrations. - [Migration Overview](/docs/migrations): Choose a source framework, distinguish automated from manual migrations, and follow a safe migration workflow. - [Migrate from Next.js](/docs/migrations/nextjs): Move App Router files, common Next.js imports, middleware, scripts, and Farm setup. - [Migrate from SvelteKit](/docs/migrations/sveltekit): Map SvelteKit routes, layouts, load functions, endpoints, hooks, environment variables, and adapters into Farm. - [Migrate from Nuxt](/docs/migrations/nuxt): Map Nuxt pages, layouts, server routes, data composables, middleware, runtime configuration, and Nitro output into Farm. - [Migrate from TanStack Start](/docs/migrations/tanstack): Convert TanStack Router file routes, route paths, component exports, scripts, and Farm setup. ### Reference CLI, testing, examples, and package map. - [CLI](/docs/cli): Use the Farm CLI to run, build, generate types, migrate apps, deploy output, and add integrations. - [Examples](/docs/examples): Use the examples folder as executable docs for routing, RSC, docs, markdown, auth, billing, email, jobs, and API keys. - [Reference](/docs/reference): A compact map of the main package exports and where to learn more. --- ## Bundle Analyzer URL: /docs/plugins/analyzer Understand the client and server weight of a Farm production build, connect initial assets to emitted pages, and enforce size limits in CI. # Bundle Analyzer `@farm.js/analyzer` explains the final production output in Farm terms. It separates browser and server code, connects emitted HTML pages to their initial JavaScript and CSS, and can fail a build when a readable size limit is exceeded. ## Install ```bash pnpm add -D @farm.js/analyzer ``` ## Start with no configuration ```ts title="farm.config.ts" import { analyzer } from "@farm.js/analyzer"; import { defineConfig } from "@farm.js/core"; export default defineConfig({ plugins: [analyzer()], }); ``` Run a production build. Farm writes the visual report to `.farm/analyze.html`: ```bash pnpm farm build ``` The report shows raw, gzip, and Brotli sizes for: - Each emitted HTML page and the initial JavaScript and CSS it loads - Every client JavaScript and CSS bundle - JavaScript in the final server output - Images, fonts, data, and other public assets Static imports are followed, but lazy `import()` chunks are not charged to a page's initial load. They remain visible in the complete client bundle list. ## Open the report automatically Use `open` during local performance work: ```ts analyzer({ open: true }); ``` It is off by default so CI and remote builds do not try to launch a browser. ## Protect size in CI The limits use names that match the report: ```ts title="farm.config.ts" analyzer({ json: true, limits: { page: "200kb", asset: "100kb", client: "500kb", server: "2mb", }, }); ``` | Limit | What it measures | | -------- | ----------------------------------------------------- | | `page` | Initial JavaScript and CSS for each emitted HTML page | | `asset` | Each individual emitted JavaScript or CSS file | | `client` | All emitted client JavaScript and CSS | | `server` | All JavaScript found in the final server output | Limits use gzip size by default. A number means bytes; a string accepts `b`, `kb`, `mb`, or `gb`. An exceeded limit fails the production build after the report is written, so the failure remains inspectable. Use Brotli instead, or report limit failures without stopping the build: ```ts analyzer({ metric: "brotli", onLimit: "warn", limits: { page: "180kb" }, }); ``` ## Save machine-readable results `json: true` writes `.farm/analyze.json` next to the default HTML report. It includes the report, configured limits, and every violation. Pass a path when another tool expects a specific location: ```ts analyzer({ output: "reports/build.html", json: "reports/build.json", }); ``` Set `output: false` when only JSON and build limits are needed: ```ts analyzer({ output: false, json: true, limits: { client: "500kb" }, }); ``` ## Options Adding `analyzer()` to `plugins` enables analysis. Remove it from the array when analysis is not needed. | Option | Type | Default | Purpose | | --------- | ----------------------------- | -------------------- | -------------------------------------------- | | `output` | `string \| false` | `.farm/analyze.html` | Project-relative HTML report path, or none. | | `json` | `boolean \| string` | `false` | Write companion JSON or use a relative path. | | `open` | `boolean` | `false` | Open the HTML report after the build. | | `metric` | `"raw" \| "gzip" \| "brotli"` | `"gzip"` | Compression metric used by limits. | | `limits` | `AnalyzerLimits` | `{}` | Optional page, asset, client, server limits. | | `onLimit` | `"error" \| "warn"` | `"error"` | Fail the build or print warnings. | ## How page attribution works Farm reads emitted HTML, resolves its script, stylesheet, module-preload, and preload references, then follows static JavaScript and CSS imports. A base path in a public URL is normalized back to the emitted asset, and shared chunks are counted once per page. This is exact for HTML that exists in the production output. A dynamic SSR route has no standalone HTML file to inspect, and its server work can depend on the request. The analyzer therefore does not invent a route number for it. Its client chunks remain in the client total and its runtime code remains in the server total. --- ## Blyp URL: /docs/plugins/blyp Add request-scoped logging, trace propagation, and Farm lifecycle and browser telemetry with the community-maintained Blyp plugin. # Blyp [Blyp](https://www.blyp.dev) is a community-maintained logging and telemetry plugin. It connects Farm's request lifecycle to request-scoped logs, trace propagation, and server and browser telemetry. The plugin is maintained by Blyp outside the Farm project; see the [Blyp Farm.js guide](https://www.blyp.dev/docs/integrations/farmjs) for its full configuration reference. ## Install Add Blyp to an existing Farm application: ```bash pnpm add @blyp/core ``` The Farm adapter supports Node.js and Bun runtimes. Blyp's server logger and connector lifecycle require these runtimes; known edge-only presets fail during configuration. ## Register the plugin Register `blypPlugin()` once in `farm.config.ts`. Append it to your existing plugins: ```ts import { defineConfig } from "@farm.js/core"; import { blypPlugin } from "@blyp/core/farmjs"; export default defineConfig({ plugins: [ blypPlugin({ level: "info", telemetry: { events: "curated", browser: { sampleRate: 0.1, }, }, }), ], }); ``` Blyp automatically logs successful requests, error responses, and thrown errors. It preserves the response body and existing headers and adds an `x-blyp-trace-id` response header. ## Log inside server routes Use the request-aware `logger` from `@blyp/core/farmjs` in a server API route such as `src/app/api/health/route.ts`: ```ts import { logger } from "@blyp/core/farmjs"; export async function GET() { logger.info("health check completed"); return Response.json({ ok: true }); } ``` The logger uses the active request context in pages, API routes, middleware, and server actions. Outside a request, it falls back to the logger configured by the plugin. Keep this import in server code. Child loggers and structured logs retain the request trace and authentication context. Emitting a structured log with `logger.createStructuredLog()` suppresses the automatic HTTP record for that request. See [Blyp's guide](https://www.blyp.dev/docs/integrations/farmjs) for structured logging examples. ## Request traces Blyp selects the request trace ID from the incoming Blyp trace header, then an active Farm or OpenTelemetry trace ID, then a newly generated ID. The default propagation and response header is `x-blyp-trace-id`; use `traceHeader` to select another header: ```ts blypPlugin({ traceHeader: "x-request-trace", }); ``` Only the trace ID is exposed to Farm page props. The scoped logger and authentication data stay in Farm's private request store. ## Lifecycle telemetry Use `telemetry.events` to choose which Farm observability events Blyp forwards: | Value | Behavior | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `"curated"` | The default: server and build milestones, route generation summaries, validation failures, not-found events, warnings, and framework errors. | | `"all"` | Every non-request Farm event. | | `FarmEventType[]` | Only the specified Farm event types. | | `false` | Disable Farm event forwarding. | Farm `request.*` events are excluded because Blyp's request hooks already emit HTTP records. Setting `telemetry.events: false` leaves browser telemetry enabled; set `telemetry: false` to disable both lifecycle and browser telemetry while retaining HTTP request logging. ## Browser telemetry Browser telemetry is enabled by default. It captures completed hydration, rendered navigations, navigation failures, browser errors, unhandled rejections, and allowlisted performance entries. Non-error events are sampled once per page session: development uses a 100% sample rate and production defaults to 10%. Errors and navigation failures are never sampled out. To disable browser telemetry while retaining server lifecycle events: ```ts blypPlugin({ telemetry: { events: "curated", browser: false, }, }); ``` The plugin handles the configured client-ingestion endpoint directly, so no separate Farm API route is required. Browser telemetry excludes request bodies, query values, URL hashes, page data, document titles, referrers, route parameters, and arbitrary performance resource URLs. See the [upstream guide](https://www.blyp.dev/docs/integrations/farmjs) for ingestion, connectors, and enrichment options. ## Verify the setup Start your application and request `/api/health`. Confirm the response contains `x-blyp-trace-id`, your application log appears, and Blyp emits one automatic HTTP record with the request's method, status, duration, and trace ID. If HTTP records appear twice, check that `blypPlugin()` is registered only once and that you are not also manually logging the request summary. An emitted structured request log replaces the automatic HTTP record. To remove the plugin, remove its registration from `farm.config.ts` and any Blyp logger imports or custom client telemetry wiring. --- ## Client Plugin API URL: /docs/plugins/client Define typed hydration, navigation, error, performance, and cleanup hooks directly inside a Farm plugin. # Client Plugin API A Farm plugin can own server and browser behavior through one `definePlugin()` call. Add lifecycle hooks directly under `client`; Farm extracts those hooks into its generated browser runtime in development and production. Application code does not mount a provider, initialize a second registry, or forward router events. ## Define both sides **farm.config.ts** ```ts import { defineConfig, definePlugin } from "@farm.js/core"; const analytics = definePlugin({ name: "acme:analytics", version: "1.0.0", enforce: "post", setup() { return { // This value stays on the server. serverToken: process.env.ANALYTICS_TOKEN, }; }, runtime: { after({ response }) { return response; }, }, client: { public: { projectId: "storefront", sampleRate: 0.25, }, async setup({ plugin, public: config, router, isDev }) { const { createAnalytics } = await import("@acme/analytics/browser"); const analytics = createAnalytics(config); return { analytics, pluginName: plugin.name, debug: isDev, navigate: router.navigate.bind(router), }; }, hydration: { before({ state, mode }) { state.analytics.event(`hydration:${mode}`); }, after({ state, durationMs, recovered }) { state.analytics.event("hydrated", { durationMs, recovered }); }, }, navigation: { before({ state, to, signal }) { if (!signal.aborted) state.analytics.event("before", { path: to.pathname }); }, loaded({ state, to }) { state.analytics.event("loaded", { path: to.pathname }); }, resolved({ state, to }) { state.analytics.event("resolved", { path: to.pathname }); }, rendered({ state, to, durationMs }) { state.analytics.page(to.href, { durationMs }); }, error({ state, error, to }) { state.analytics.error(error, { path: to.pathname }); }, }, error({ state, error, phase }) { state.analytics.error(error, { phase }); }, close({ state, reason }) { state.analytics.flush(reason); }, }, }); export default defineConfig({ plugins: [analytics], }); ``` The value returned by `client.setup` becomes typed `state` in every later client hook. The shape of `client.public` is inferred and available as read-only `public` data in every event. Server `setup` state and client `setup` state are intentionally separate. Neither is merged into a global application object. ## Browser boundary Farm extracts only known properties under `client` into a generated browser module. A client hook does not close over the surrounding server module. Everything a client hook needs should come from: - the hook event, including `public`, `router`, and route information - browser globals such as `window` and `document` - a dynamic import made inside a client hook - state returned by `client.setup` ```ts client: { public: { release: "2026.07" }, async setup({ public: config }) { const { startReporter } = await import("./reporter.browser"); return startReporter(config.release); }, navigation: { rendered({ state, to }) { state.page(to.pathname); }, }, }, ``` Do not reference a server module variable from a client hook. Put intentionally public values in `client.public`, import browser dependencies inside a hook, and return reusable browser helpers from `client.setup`. ## Lifecycle reference | Hook | When it runs | | ---------------------------- | ----------------------------------------------------------------------------------------------------- | | `client.setup` | Once when Farm starts the generated browser runtime. May return typed private client state. | | `client.hydration.before` | Immediately before Farm hydrates or initially renders a hydratable route. | | `client.hydration.after` | After React accepts the initial hydrate or render operation. | | `client.navigation.before` | After navigation blockers pass and before route data or code loads. | | `client.navigation.loaded` | After route data, route code, or server HTML is ready. | | `client.navigation.resolved` | After Farm updates history and commits the navigation render. | | `client.navigation.rendered` | After the browser receives two animation frames for the committed route. | | `client.navigation.error` | When loading or committing that navigation fails. | | `client.error` | For hook failures, browser errors, unhandled rejections, hydration failures, and navigation failures. | | `client.performance` | For supported browser `PerformanceEntry` values. Farm observes only when this hook exists. | | `client.close` | During page hide, HMR disposal, or explicit runtime cleanup. | Hydration events include `mode: "hydrate" | "render"`, the root `container`, timing, and whether Farm recovered. A server-only page still starts client plugins but does not emit hydration hooks when no browser render occurs. Navigation events include: - `id`, `from`, `to`, and `action` - an `AbortSignal` aborted when a newer navigation supersedes the current one - matched route `pattern` and `params` when available - `durationMs` after loading begins - loaded route data in `navigation.loaded` Farm skips later lifecycle hooks for an aborted navigation. ## Shared event values | Value | Meaning | | ------------------ | -------------------------------------------------------------------------------- | | `plugin` | Stable plugin `name` and optional `version`. | | `public` | Read-only JSON-safe data declared as `client.public`. | | `router` | Active client router with `navigate`, optional `prefetch`, and navigation state. | | `isDev` / `isProd` | Current browser build mode. | | `deploymentId` | Farm deployment identifier when available. | | `state` | This plugin's `client.setup` result, available after setup. | ## Ordering and failures Client hooks use the parent plugin's `enforce` order: 1. `pre` plugins 2. normal plugins 3. `post` plugins Farm runs hooks serially in that order. Cleanup runs in reverse order. A failed hook does not stop sibling plugins or application rendering; Farm logs it and sends it to other client error hooks with a phase such as `plugin:navigation.before`. ## Security boundary The browser entry is an allowlisted extraction, not a serialization of the whole plugin. - Farm copies only `setup`, `hydration`, `navigation`, `error`, `performance`, and `close` from `client`. - Server hooks, server setup state, request context, imports, and environment values are not copied. - Only `client.public` data crosses the server-to-browser boundary. - Public data must contain JSON-safe scalars, dense arrays, and plain objects. - Farm rejects functions, symbols, bigint values, non-finite numbers, class instances, dates, `undefined`, accessors, hidden properties, sparse arrays, and circular references. - Farm rejects unknown lifecycle fields, accessors, and native or bound hook functions. - Every public value is visible to anyone who can load the application JavaScript. ## Published plugins A package exposes one ordinary plugin factory: ```ts import { definePlugin } from "@farm.js/core"; export function analyticsPlugin(projectId: string) { return definePlugin({ name: "acme:analytics", client: { public: { projectId }, async setup({ public: config }) { const { createAnalytics } = await import("./analytics.browser.js"); return createAnalytics(config.projectId); }, navigation: { rendered({ state, to }) { state.page(to.href); }, }, }, }); } ``` Use a modern JavaScript target when publishing inline lifecycle hooks so function source remains available to Farm's build extraction. ## Good use cases - navigation and Web Vitals instrumentation - client error reporting and release metadata - view-transition coordination - browser capability detection - devtools panels and HMR diagnostics - route progress UI owned by framework tooling - client cache observers shared by a server plugin Product SDK providers and typed product endpoints still belong in an [integration](/docs/integrations). Use a client plugin when behavior follows Farm's browser lifecycle across unrelated applications. ## Production checklist - Keep `client.setup`, `navigation.before`, and `navigation.loaded` short. - Respect navigation cancellation for optional async work. - Sample high-volume performance and navigation telemetry. - Flush best-effort telemetry in `client.close`, but do not make correctness depend on page-hide delivery. - Test hydration, SPA navigation, cancellation, failure isolation, cleanup, and a production preset build. Continue with [Create a Plugin](/docs/plugins/create-plugin) for server lifecycle and packaging patterns. --- ## Content URL: /docs/plugins/content Load local Markdown, MDX, JSON, and YAML as schema-validated, typed server content without a second config file. # Content `@farm.js/content` turns local Markdown, MDX, JSON, and YAML files into typed server data. Farm discovers the files, validates them while configuring the application, watches them in development, and bundles the validated result into production output. Collections are ordinary plugin options in `farm.config.ts`. ## Install ```bash pnpm add @farm.js/content zod ``` Zod is optional. The plugin accepts Standard Schema validators, including Valibot and ArkType, or a small custom validator with `parse` or `parseAsync`. ## Define a collection **farm.config.ts** ```ts import { collection, content, files } from "@farm.js/content"; import { defineConfig } from "@farm.js/core"; import { z } from "zod"; const post = z.object({ title: z.string().min(1), description: z.string().min(1), publishedAt: z.coerce.date(), tags: z.array(z.string()).default([]), draft: z.boolean().default(false), }); export default defineConfig({ plugins: [ content({ collections: { posts: collection({ source: files("content/posts/**/*.{md,mdx}"), schema: post, transform: ({ data, words }) => ({ ...data, readingMinutes: Math.max(1, Math.ceil(words / 220)), }), }), }, }), ], }); ``` Adding `content()` enables the plugin. Remove it from `plugins` to disable content processing. There is no separate `enabled` flag. Content source files must resolve inside the app root, including when a glob's directory contains a symlink. Links to files elsewhere in the app keep their logical entry IDs and are watched for changes; links that resolve outside the app are rejected before the file is read. The schema validates frontmatter for Markdown and MDX. For JSON and YAML, it validates the complete file value. `transform` runs after validation and can add or replace fields. Its output becomes the inferred type returned to the application. Markdown and MDX frontmatter is data only: use YAML (the default, or `---yaml` / `---yml`) or JSON (`---json`). Executable formats such as `---js` and `---javascript` are rejected before parsing or schema validation, including empty frontmatter blocks. Put computed metadata in `transform` instead. Asset declarations are separate from `schema`. A schema string remains a string; Farm never changes its output into image metadata behind the validator's back. ## Write content **content/posts/hello-farm.md** ```md --- title: Hello, Farm description: A first typed post. publishedAt: 2026-09-09 tags: - Farm.js --- The Markdown body remains available separately from validated frontmatter. ``` ## Manage local assets Use `assets: true` when a collection only needs relative assets from Markdown or MDX body syntax: ```ts collection({ source: files("content/posts/**/*.{md,mdx}"), schema: z.object({ title: z.string() }), assets: true, }); ``` URL-encode filename characters such as `#`, `?`, and `%`: `[Guide](./guide%23v1.pdf)` refers to `guide#v1.pdf`. Farm gives these assets a safe generated filename before passing them through Vite's normal asset pipeline. Their original metadata names, URL query strings, fragments, and development reload behavior are preserved. The generated copies live under `.farm/content/assets` and are recreated when the app's generated `.farm` directory is cleared. Farm recognizes inline and reference-style Markdown images and file links: ```md ![Architecture](./architecture.png) [Download the guide](./guide.pdf) ![Release diagram][release-diagram] [release-diagram]: ./release-diagram.svg ``` Reference labels follow Markdown's case-insensitive matching. If a label is defined more than once, the first definition wins; later duplicates are left unchanged and do not add asset imports or missing-file errors. Relative image destinations are checked as real images with intrinsic dimensions. Relative file links are managed when their destination has a file extension; extensionless links and links to `.md` or `.mdx` remain content navigation. Remote URLs, root-relative URLs, fragments, data URLs, and dynamic MDX expressions are left unchanged. Static `src` or `href` attributes inside JSX and HTML are not interpreted in this first API; use Markdown syntax or a normal module import there. For `.mdx` files, JavaScript strings, comments, imports/exports, and JSX attributes are not scanned for Markdown assets. For example, `{"![Example](./missing.png)"}` stays literal code and does not require that image to exist. Actual Markdown images and links nested inside JSX are still managed. Farm parses MDX syntax without executing expressions or importing modules; invalid syntax reports the content filename. Ordinary `.md` files keep Markdown parsing rules. The body keeps its original Markdown except for managed destinations, which become content-hashed public URLs. `entry.bodyAssets` contains the corresponding image or file metadata for a custom Markdown component mapping. Missing assets report the content file plus the Markdown line and column. Asset changes participate in development reloads. ### Type frontmatter assets Declare frontmatter assets by the field names used by the application. They do not belong in the ordinary schema: ```ts import { asset, collection, content, files } from "@farm.js/content"; import { defineConfig } from "@farm.js/core"; import { z } from "zod"; export default defineConfig({ plugins: [ content({ collections: { posts: collection({ source: files("content/posts/**/*.{md,mdx}"), schema: z.object({ title: z.string(), description: z.string(), }), assets: { image: asset.image().optional(), downloads: asset.files().default([]), }, }), }, }), ], }); ``` ```md --- title: Introducing Farm Content description: A typed local publishing workflow. image: ./preview.png downloads: - ./guide.pdf - ./example.zip --- The frontmatter remains concise while its resolved values are fully typed. ``` The asset fields are removed before the Standard Schema validator runs, resolved relative to the content file, then merged back into `entry.data`: ```ts post.data.title; // string post.data.image; // ContentImageAsset | undefined post.data.downloads; // readonly ContentFileAsset[] ``` `asset.image()` validates one required image. `asset.images()` validates an array of images; `asset.file()` and `asset.files()` provide the equivalent generic-file forms. Add `.optional()` or `.default(...)` when the frontmatter value may be omitted. Declarations can be nested to match a nested frontmatter object. Images expose `src`, `width`, `height`, `type`, `bytes`, `name`, and the original `source`. Their shape can be passed directly to Farm's `Image` component. Files expose the same fields except for image dimensions. All managed paths must be relative, resolve to ordinary files inside the Farm project, and use forward slashes. Keep remote or public-root URLs in the ordinary schema when the application intentionally owns those strings. ## Entry IDs Entry IDs come from the path below the common static directory of the source globs. The extension and a trailing `index` are removed: | Source file | Entry ID | | ------------------------------------------ | ------------------------ | | `content/posts/hello-farm.md` | `hello-farm` | | `content/posts/guides/getting-started.mdx` | `guides/getting-started` | | `content/posts/guides/index.md` | `guides` | Pass `files(pattern, { base: "content" })` when the ID should use an explicit base. The `ignore` option accepts one glob or an array of globs. ## Read typed entries Use the server entry from a Server Component, static path function, server query, server function, or API route. ```tsx import { getCollection } from "@farm.js/content/server"; export default async function BlogPage() { const posts = (await getCollection("posts")) .filter((entry) => !entry.data.draft) .sort((left, right) => right.data.publishedAt.getTime() - left.data.publishedAt.getTime()); return ( <ul> {posts.map((entry) => ( <li key={entry.id}> {entry.data.title} · {entry.data.readingMinutes} min </li> ))} </ul> ); } ``` Each entry contains: | Field | Meaning | | ------------ | ------------------------------------------------------------------------- | | `id` | Stable path-derived identifier used with `getEntry`. | | `data` | Validated schema output, followed by the optional transform. | | `body` | Markdown or MDX body. Managed asset destinations contain emitted URLs. | | `bodyAssets` | Image and file metadata discovered from managed Markdown body syntax. | | `filePath` | Project-relative source path, useful for diagnostics and editorial tools. | Read one entry when the route already has its ID: ```ts import { getEntry, getEntryOrThrow } from "@farm.js/content/server"; const optionalPost = await getEntry("posts", "guides/getting-started"); const post = await getEntryOrThrow("posts", "guides/getting-started"); ``` `getEntry` returns `undefined` for a miss. `getEntryOrThrow` includes the collection and ID in its error, which is usually better for a path Farm already generated from the same collection. ## Generate static pages Use a catch-all route when IDs can contain `/`. **src/app/posts/[...slug]/page.tsx** ```tsx import { getCollection, getEntryOrThrow } from "@farm.js/content/server"; import type { PageProps } from "@farm.js/core"; export const dynamic = "force-static"; export async function getStaticPaths() { return (await getCollection("posts")).map((entry) => ({ slug: entry.id.split("/"), })); } export default async function PostPage({ params }: PageProps<"/posts/[...slug]">) { const post = await getEntryOrThrow("posts", params.slug); return <article>{post.body}</article>; } ``` ## Use a hosted CMS A collection's source does not have to be files. `remote()` is the one interface every CMS, API, or database plugs into - implement it once and the rest of this page applies unchanged: the same schema validation, transforms, generated types, and server reads and writes. ### The source interface ```ts import { collection, content, remote } from "@farm.js/content"; source: remote({ // names the source in error messages, e.g. "mycms:posts" name: "mycms:posts", // REQUIRED - resolve to an array of documents: // id: stable, route-friendly, unique in the collection (a slug) // data: goes through the collection schema, so YOUR schema is the boundary // body: optional Markdown, for entry.body and word counts fetch: async () => [{ id, data, body }], // OPTIONAL - dev only: re-fetch this often and reload when documents changed refreshInterval: 30_000, // OPTIONAL - the write surface, one callback per verb, same shape as fetch. // Each returns the provider's CONFIRMED document; omit any verb and // collections.<name>.<verb>() says exactly what is missing. create: async ({ data, body }) => confirmedDocument, update: async (id, patch) => confirmedDocument, delete: async (id) => {}, }), ``` That is the entire integration surface. Everything below holds for every provider: - **Snapshot semantics.** Documents are fetched while the configuration loads, validated, and bundled into production output. Publishing means rebuilding: point the provider's webhook at a deploy hook. Entries sort by ID so fetch order never churns the generated module. - **Development.** `refreshInterval` polls and reloads only when documents actually changed. A successful write rebuilds the snapshot immediately. - **Writes.** `collections.<name>.create/update/delete` route through the callbacks, and the confirmed document is re-validated against the collection schema before anyone trusts it. In production the write lands in the CMS while the running deployment serves the bundled snapshot until the next build. Gate every write behind your own [server function](/docs/api-client): Farm never exposes content writes to the browser. - **Limits.** Remote collections cannot declare [typed asset fields](#manage-local-assets), which resolve files on disk. ### Reading and writing entries ```ts import { collections } from "@farm.js/content/server"; const posts = await collections.posts.all(); // the validated snapshot await collections.posts.update("hello-world", { data: { title: "New title" } }); await collections.posts.create({ data: { title: "Drafted from the app" } }); await collections.posts.delete("old-post"); ``` ### Official sources Provider packages implement the interface for you - each is one import plus its credentials, and everything above applies as written: | Provider | Source | Setup | | ---------- | --------------------------------------------- | ----------------------------------------------------------------------------------------- | | Sanity | `sanitySource` from `@farm.js/sanity` | [Sanity integration → Content collections](/docs/integrations/sanity#content-collections) | | Contentful | `contentfulSource` from `@farm.js/contentful` | [below](#contentful-setup) | #### Contentful setup ```ts import { contentfulSource } from "@farm.js/contentful"; posts: collection({ source: contentfulSource({ contentType: "post", query: { order: ["-sys.createdAt"] } }), schema: post, }); ``` - credentials resolve from `CONTENTFUL_SPACE_ID` and `CONTENTFUL_ACCESS_TOKEN`, or pass an existing client. - entry IDs default to a string `fields.slug`, falling back to `sys.id`. - pagination past Contentful's 1000-entry page cap is handled internally, bounded by the reported total; `limit` caps the total entries loaded. `content_type` and `skip` are owned by the source, everything else in `query` passes through. - set `host: "preview.contentful.com"` with `CONTENTFUL_PREVIEW_TOKEN` to load drafts. - the source is read-only for now: Contentful writes go through its separate management SDK in a [server function](/docs/api-client). ### Bring your own CMS Any system that can answer "give me the documents" plugs in the same way - here is the whole direction, using WordPress's REST API as a stand-in for whatever you run. No Farm package, no SDK, just the interface: ```ts import { collection, content, remote } from "@farm.js/content"; const wordpress = remote({ name: "wp:posts", fetch: async () => { const posts = await fetch("https://blog.example.com/wp-json/wp/v2/posts?per_page=100").then( (response) => response.json(), ); return posts.map((post) => ({ id: post.slug, // 1. pick a stable, route-friendly id data: { title: post.title.rendered, publishedAt: post.date }, // 2. shape data for YOUR schema body: post.content.rendered, // 3. optional body })); }, refreshInterval: 30_000, // 4. dev reloads when documents change // 5. optional: implement writes and collections.<name>.update() lights up update: async (id, patch) => { const updated = await wpApi(`posts?slug=${id}`, { method: "POST", body: patch.data }); return { id, data: updated }; }, }); ``` The direction, in order: 1. **Map documents** to `{ id, data, body? }` in `fetch` - the provider's SDK, a REST call, a database query, anything. 2. **Pick stable IDs** - a slug beats an internal database id, because entry IDs become routes. 3. **Let your schema be the boundary** - shape `data` for the collection schema and consumers never learn which CMS is behind it. Swapping providers later changes only the `source:` line. 4. **Wire updates** - the provider's publish webhook points at your deploy hook, and `refreshInterval` covers development. 5. **Add writes when the app needs them** - one callback per verb, each returning the provider's confirmed document, gated behind your own server functions. #### Even the official providers work this way The packages under `@farm.js/*` are the same interface published with conventions attached - nothing more. The callbacks are ordinary functions, so the natural way to bring your own Sanity is your own `@sanity/client` instance: GROQ for reads (mutations are not GROQ - Sanity's mutation API is `create`/`patch`/`delete`, in the SDK and over HTTP alike), and the client's mutation methods for writes: ```ts import { createClient } from "@sanity/client"; const sanity = createClient({ projectId: process.env.SANITY_PROJECT_ID!, dataset: process.env.SANITY_DATASET!, apiVersion: "2026-03-01", token: process.env.SANITY_API_WRITE_TOKEN, // server-only; enables the write callbacks useCdn: false, // builds want the freshest documents }); // The collection's read set and its id mapping, shared with the write // callbacks below. const postsQuery = `*[_type == "post"]{ _id, slug, title, publishedAt }`; const entryId = (doc) => doc.slug?.current ?? doc._id; // Mutations address `_id`, entry ids are slugs: resolve the bridge against the // same query the collection reads, so a write can only ever land on a document // this collection owns. A bare `*[slug.current == $id][0]._id` would happily // resolve an unrelated `_type` that shares the slug - and then `delete` it. const resolveDocumentId = async (id) => { const docs = await sanity.fetch(postsQuery); const match = docs.find((doc) => entryId(doc) === id); if (!match) throw new Error(`sanity:posts has no document for ${id}`); return match._id; }; const sanityPosts = remote({ name: "sanity:posts", fetch: async () => { const docs = await sanity.fetch(postsQuery); return docs.map((doc) => ({ id: entryId(doc), data: doc })); }, update: async (id, patch) => { const updated = await sanity .patch(await resolveDocumentId(id)) .set(patch.data) .commit(); return { id, data: updated }; }, create: async ({ data }) => { const doc = await sanity.create({ _type: "post", ...data }); return { id: entryId(doc), data: doc }; }, delete: async (id) => { await sanity.delete(await resolveDocumentId(id)); }, }); ``` This is, almost line for line, what `sanitySource` does for you - it only adds the env conventions, this write-target resolution, and the error naming. The same also works with no SDK at all, over Sanity's plain HTTP API: ```ts const sanityApi = `https://${process.env.SANITY_PROJECT_ID}.api.sanity.io/v2026-03-01`; const dataset = process.env.SANITY_DATASET; const sanityPosts = remote({ name: "sanity:posts", fetch: async () => { const query = encodeURIComponent(`*[_type == "post"]{ _id, slug, title, publishedAt }`); const { result } = await fetch(`${sanityApi}/data/query/${dataset}?query=${query}`).then( (response) => response.json(), ); return result.map((doc) => ({ id: doc.slug?.current ?? doc._id, data: doc })); }, // Mutations are one endpoint away; the write surface, schema re-validation, // and dev rebuild behave identically to the SDK version above. update: async (id, patch) => { const response = await fetch(`${sanityApi}/data/mutate/${dataset}?returnDocuments=true`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${process.env.SANITY_API_WRITE_TOKEN}`, }, body: JSON.stringify({ mutations: [ // Constrained to the collection's own `_type`, for the same reason // the SDK version resolves against its read query. { patch: { query: `*[_type == "post" && slug.current == $id][0]`, params: { id }, set: patch.data, }, }, ], }), }); const { results } = await response.json(); return { id, data: results[0].document }; }, }); ``` Contentful reads inline the same way: ```ts fetch: async () => { const url = `https://cdn.contentful.com/spaces/${process.env.CONTENTFUL_SPACE_ID}` + `/environments/master/entries?access_token=${process.env.CONTENTFUL_ACCESS_TOKEN}&content_type=post`; const { items } = await fetch(url).then((response) => response.json()); return items.map((item) => ({ id: item.fields.slug ?? item.sys.id, data: item.fields })); }, ``` What the inline versions leave to you is exactly what the packages add: pagination past provider page caps, linked-entry resolution, slug-to-id mapping for writes, lazy credential errors, and preview switches. Use whichever tier fits - they are interchangeable, and this path is covered by the plugin's own test suite, so it stays working. A dedicated `@farm.js/<provider>` package is only ever ergonomics on top of this: conventional env vars, a default id, provider-aware errors. Start with `remote()`; extract the package when the conventions settle. ### Publishing a source package Anyone can ship an adapter - a CMS vendor, a community author. Farm's own `sanitySource` and `contentfulSource` use nothing a third party cannot: they import exactly two **type-only** names from `@farm.js/content` and return the plain `remote()` shape. The conventions that make an adapter feel first-party: ```ts import type { ContentRemoteDocument, ContentRemoteSource } from "@farm.js/content"; export function myCmsSource(options: MyCmsSourceOptions): ContentRemoteSource { // ... } ``` 1. **Type-only dependency.** Import only types from `@farm.js/content`; declare it as an optional peer. Your package then has zero runtime coupling to Farm and works with every version that keeps the shape. 2. **The provider SDK is a peer dependency** with literal imports, so bundlers can trace it and apps control the version. 3. **Resolve credentials lazily**, from conventional env vars (`MYCMS_TOKEN`) with explicit options and an existing-client escape hatch. Importing `farm.config.ts` without the env set must not throw; the first `fetch()` should, with a message naming the env vars. 4. **Pick route-friendly default IDs** (a slug over an internal id) and document the fallback. If writes address documents differently than reads identify them, resolve the mapping inside the source, the way `sanitySource` maps slugs back to `_id`. 5. **Name errors after yourself.** Every thrown message should carry the source `name`, so a failing build says which provider and which document. 6. **Implement writes only when the provider can confirm them.** Each verb returns the stored document, which Farm re-validates against the collection schema. Omit verbs you cannot honor - the collection handle explains what is missing better than a broken write. 7. **Test against the real SDK shape** - the reference suites in `packages/farm-sanity` and `packages/farm-contentful` cover fetching, id derivation, pagination, credential failures, and write flows, and are the template worth copying. An adapter that follows these is indistinguishable from an official one. Open a pull request to add it to the table above. For **live content** that must update without a rebuild, keep the provider SDK in a server-only module and fetch through a [Server Query](/docs/server-queries) instead. Farm can validate the response, cache published content, and share the typed result with server or client consumers. For example, a Sanity-backed query can stay small: **src/lib/cms.ts** ```ts import { createServerQuery } from "@farm.js/core/server-query"; import { createClient } from "@sanity/client"; import { z } from "zod"; const Post = z.object({ id: z.string(), title: z.string(), slug: z.string(), }); const sanity = createClient({ projectId: process.env.SANITY_PROJECT_ID!, dataset: process.env.SANITY_DATASET!, apiVersion: "2026-09-09", useCdn: true, }); export const cmsPosts = createServerQuery({ output: z.array(Post), key: () => ["cms", "posts"], staleTime: "5m", handler: () => sanity.fetch(`*[_type == "post"]{ "id": _id, title, "slug": slug.current }`), }); ``` A Server Component can call `await cmsPosts()`. Point a verified CMS webhook at a Farm API route and call `await invalidate(["cms", "posts"])` when published content changes; see [Cache and PPR](/docs/cache-ppr). For draft previews, use a server-only read token, bypass the provider CDN, authorize the request, and avoid a shared persistent cache. Never expose a CMS token to a Client Component. The same shape works with another provider: replace the client and query language, while keeping the server-only boundary, output schema, cache key, and invalidation path. See the [Sanity JavaScript client guide](https://www.sanity.io/docs/apis-and-sdks/js-client-getting-started) for provider-specific setup. ## Type generation Farm adds content inference to the same `src/farm.d.ts` used for routes and environment values. It reads the plugin type from `farm.config.ts`, so collection keys, Zod output, coerced `Date` values, and transform results flow into `getCollection` and `getEntry` without a handwritten registry. Development and production builds refresh the file automatically. Run this after changing config if an editor is open before the development server: ```bash farm generate ``` Commit `src/farm.d.ts` when the application already commits Farm's generated declarations. ## Split a large setup An inline config is the shortest starting point. If collections grow, extract ordinary values to a server-owned application module rather than introducing another framework config: **src/content.ts** ```ts import { collection, files } from "@farm.js/content"; import { z } from "zod"; export const collections = { posts: collection({ source: files("content/posts/**/*.md"), schema: z.object({ title: z.string() }), }), }; ``` **farm.config.ts** ```ts import { content } from "@farm.js/content"; import { defineConfig } from "@farm.js/core"; import { collections } from "./src/content"; export default defineConfig({ plugins: [content({ collections })] }); ``` The plugin still receives the collection through its options. The extra file is only organization chosen by the application. ## Rendering and security The plugin returns `body` text and does not execute MDX or emit HTML automatically. Without the `assets` option, the body is unchanged. With asset handling enabled, only recognized Markdown destinations are rewritten. This keeps the collection renderer-neutral and lets the application choose its component mapping, Markdown renderer, and sanitization policy. Treat author-provided HTML as untrusted unless the source is controlled and the chosen renderer sanitizes it. `@farm.js/content/server` fails the client build when imported into a browser environment. Use a [Server Query](/docs/server-queries) or API route when a Client Component needs content data. ## Development and production - Development regenerates content after supported source files are added, changed, or removed, then reloads the current page. Referenced asset changes do the same. Validation failures appear in Vite's error overlay with the collection, source file, field path or Markdown position, and validation message. - Production serializes only validated values into a private `.farm/content/server.mjs` build input. Nitro bundles that module, so the deployed server does not read project source files per request. Generated output must stay inside the app root. Outside or dangling output symlinks fail before Farm creates directories or writes files; output-directory links within the app remain supported. - Dates and bigints keep their runtime types. Plain objects, arrays, `undefined`, and primitives are preserved. Functions, symbols, class instances, circular structures, invalid dates, and non-finite numbers fail the build instead of being silently changed. The runnable [`examples/content-demo`](https://github.com/farming-labs/farm.js/tree/main/examples/content-demo) shows inline configuration, Zod coercion, computed reading time, nested IDs, static generation, and the finished editorial UI. --- ## Create a Plugin URL: /docs/plugins/create-plugin Build a typed Farm plugin with private state, request context, Web Request transforms, and framework lifecycle hooks. # Create a Plugin Build a plugin when behavior belongs to the framework rather than one product integration. Good examples include request tracing, security policy, HTML transforms, route analysis, deployment adapters, development tooling, and global instrumentation. ## Typed API routes Plugins can own API handlers without creating files in the app's routing directory. Declare them in `routes`, using the same API request pipeline as file routes: ```ts import { definePlugin } from "@farm.js/core"; import { z } from "zod"; export const projectRoutes = definePlugin({ name: "acme:project-routes", routes: ({ route }) => { const project = route.scope("/api/projects/[projectId]"); return [ project.post("uploads/[uploadId]", { input: { params: z.object({ projectId: z.string(), uploadId: z.string() }), body: z.object({ title: z.string().trim().min(1) }), query: z.object({ view: z.enum(["summary", "full"]).default("summary") }), headers: z.object({ "x-request-id": z.string().optional() }), }, output: z.object({ id: z.string(), title: z.string() }), handler(_request, { input }) { return { id: input.params.uploadId, title: input.body.title }; }, }), ]; }, }); ``` Register `projectRoutes` in `defineConfig({ plugins: [projectRoutes] })`. Farm mounts `POST /api/projects/[projectId]/uploads/[uploadId]`. Scopes are immutable prefix builders; they do not register a route until you return a method definition. Nested scopes work too. Paths must be literal, canonical `/api` paths without query strings or trailing slashes. All `input` fields and `output` are optional. Zod and Standard Schema validators are supported, including asynchronous refinements and transformations. Handlers receive parsed values and defaults; callers use the schema's input type for bodies, query strings, and headers. URL parameters remain strings (or arrays of strings for catch-alls) on the caller side. A params schema must describe the named parameters in the complete composed path. Invalid input produces a JSON `400` response before endpoint middleware or the handler runs. Malformed JSON and the configured request body size limit retain the normal API behavior. The optional `output` schema validates and transforms plain JSON handler results; an output validation failure is a server error, not a client `400`. Raw `Response` results bypass output validation so cookies, files, and streams are not consumed or rewritten. Do not use `output` to claim a type for arbitrary raw response bodies. Use the existing endpoint `middleware` array for authentication and authorization. A scoped project ID is routing data, not proof that the caller owns the project. Runtime plugin hooks, app middleware, HEAD/405 handling, and response headers still apply in development and Nitro production output. File and plugin routes may contribute different methods to the same path; duplicate methods and equivalent dynamic shapes fail registration. Plugin routes require the default universal production build; the legacy `universal: false` build fails with an actionable error instead of silently omitting these handlers. Route factories run synchronously during discovery/build and again at production startup. Keep paths and methods stable across environments; put per-request decisions in handlers or middleware. Production checks the runtime table against the build manifest. Restart the dev server after changing a config-owned plugin factory, and run `farm generate` to refresh checked-in artifacts. Request-driven registration and hot replacement of config-owned plugin state are not part of this initial API. Use `definePlugin()` and retain its inferred return type: annotating the value as the broad `FarmPlugin` type erases its route literals. The factory and handlers stay server-only. See [scoped API callers](/docs/api-client#scoped-dynamic-routes) for the generated browser/server caller. Existing provider integration route factories are unchanged; `routes` here adds handlers to the plugin interface, not a second integration registry. ## A complete runtime plugin This plugin creates typed private state, derives typed context for every request, forwards a request ID to the handler, and adds the ID to the response. **src/plugins/request-tracing.ts** ```ts import { definePlugin } from "@farm.js/core"; import { randomUUID } from "node:crypto"; type RequestTracingOptions = { header?: string; }; export function requestTracingPlugin(options: RequestTracingOptions = {}) { return definePlugin({ name: "acme:request-tracing", setup() { return { header: options.header ?? "x-request-id", }; }, runtime: { context({ request, req, state }) { const requestId = request.headers.get(state.header) ?? randomUUID(); req.set("requestId", requestId, { exposeToPage: true }); return { requestId }; }, before({ request, ctx, state }) { const headers = new Headers(request.headers); headers.set(state.header, ctx.requestId); return new Request(request, { headers }); }, after({ response, ctx, state }) { const headers = new Headers(response.headers); headers.set(state.header, ctx.requestId); return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }); }, error({ error, ctx }) { console.error("Request failed", { requestId: ctx.requestId, error, }); }, }, }); } ``` `state.header`, `ctx.requestId`, and their return types are inferred without manually supplying generics. ## Register it **farm.config.ts** ```ts import { defineConfig } from "@farm.js/core"; import { requestTracingPlugin } from "./src/plugins/request-tracing"; export default defineConfig({ plugins: [ requestTracingPlugin({ header: "x-acme-request-id", }), ], }); ``` Use a factory when consumers need options. Return `definePlugin()` directly so inference is preserved. ## Add browser behavior Add `client` when the same framework plugin needs hydration, navigation, browser error, performance, or cleanup hooks. Define those hooks in the same plugin object. ```ts export function requestTracingPlugin(options: RequestTracingOptions = {}) { return definePlugin({ name: "acme:request-tracing", // Server lifecycle hooks stay here. runtime: { context() {}, }, client: { public: { header: options.header ?? "x-request-id", }, setup({ public: config }) { return { header: config.header, navigations: 0 }; }, navigation: { rendered({ state, to }) { state.navigations += 1; console.log(state.header, to.pathname); }, }, }, }); } ``` Farm extracts the client hooks and generates their runtime registration in development and production. Do not put secrets in `client.public`; it is embedded in application JavaScript. Client hooks cannot close over surrounding server values, so use event data, browser globals, dynamic imports inside a hook, and state returned by `client.setup`. Read [Client Plugins](/docs/plugins/client) for every hook, lifecycle order, cancellation behavior, and packaging rules. ## Choose the plugin's scope Start local, then package the plugin when more than one application needs the same framework behavior. | Scope | Use it for | | ------------------ | ------------------------------------------------------------------------------- | | Local plugin | One application's route policy, diagnostics, or temporary framework experiment. | | Published plugin | Reusable tracing, security, rendering, build, or development behavior. | | Layer plugin | Organization defaults shipped together with shared config and integrations. | | Integration plugin | Low-level framework behavior required internally by one product integration. | A published plugin should expose an options factory and keep its setup state private: **packages/farm-plugin-security/src/index.ts** ```ts import { definePlugin } from "@farm.js/core"; export interface SecurityPluginOptions { frameAncestors?: string; } export function securityPlugin(options: SecurityPluginOptions = {}) { return definePlugin({ name: "acme:security", version: "1.0.0", setup() { return { frameAncestors: options.frameAncestors ?? "'none'", }; }, runtime: { after({ response, state }) { const headers = new Headers(response.headers); headers.set("x-content-type-options", "nosniff"); headers.set("content-security-policy", `frame-ancestors ${state.frameAncestors}`); return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }); }, }, }); } ``` Keep the public surface small: export the factory, its options, and any intentionally shared types. Consumers should not need to understand the plugin's internal state or lifecycle wiring. ## Validate options A typed factory already gives the app autocomplete. To also check options at runtime, declare them on the plugin with a [Standard Schema](https://standardschema.dev) (Zod, Valibot, ArkType, and others). `definePlugin` then returns the factory for you: ```ts import { definePlugin } from "@farm.js/core"; import { z } from "zod"; export const securityPlugin = definePlugin({ name: "acme:security", options: z.object({ frameAncestors: z.string().default("'none'"), reportUri: z.url().optional(), }), runtime: { after({ response, state }) { const headers = new Headers(response.headers); headers.set("content-security-policy", `frame-ancestors ${state.frameAncestors}`); return new Response(response.body, { status: response.status, headers }); }, }, }); ``` Apps pass typed options as usual: ```ts plugins: [securityPlugin({ frameAncestors: "'self'" })]; ``` One schema gives you three things: - The app gets the schema's input type: options autocomplete, and wrong or unknown options are type errors. - The plugin receives the parsed options, with defaults applied. Without a `setup`, they are the plugin's `state`, so every hook reads `state.frameAncestors`. - Invalid options throw a `FarmPluginOptionsError` while the config loads, listing each problem, instead of failing later inside a hook. When the plugin needs to build something from its options, `setup` and `configure` receive them, and `setup`'s return value becomes the state: ```ts export const reporting = definePlugin({ name: "acme:reporting", options: z.object({ endpoint: z.url(), sampleRate: z.number().default(1) }), setup({ options }) { return { client: createReporter(options.endpoint, options.sampleRate) }; }, }); ``` When every option is optional, the factory can be called without arguments: `securityPlugin()` validates `{}`. Plugins are created while the config loads, so the schema must validate synchronously; asynchronous refinements are rejected. ## Depend on another plugin A plugin that needs another one says so with `dependsOn`, by name (`farm:teams` or `teams`) or integration key: ```ts title="src/index.ts" export function billing() { return definePlugin({ name: "farm:billing", dependsOn: ["teams"] /* ... */ }); } ``` The app's config then fails to load, before anything runs, when a dependency is missing: ``` Plugin "farm:billing" depends on "teams", which is not configured. Add it to `plugins` (or `integrations`) in farm.config. ``` A plugin that lists itself, or plugins that depend on each other in a cycle, fail the same way. ## Transform config `configure` runs after Farm loads the app config and before it creates the development or production pipeline. Return only when the plugin needs to replace the current value. ```ts export const observabilityDefaults = definePlugin({ name: "acme:observability-defaults", configure(config) { return { ...config, observability: config.observability ?? { events: true, runtime: true, }, }; }, }); ``` Keep config transforms deterministic. Use `enforce: "pre"` or `enforce: "post"` only when another plugin's output is part of your contract. ## Own an exact endpoint Use `runtime.endpoints` when a plugin owns a small HTTP surface such as a health probe or provider callback. Paths are application-relative and Farm applies `basePath` automatically. ```ts export const statusPlugin = definePlugin({ name: "acme:status", runtime: { endpoints: [ { path: "/status", handler({ request }) { if (request.method !== "GET") { return new Response(null, { status: 405 }); } return Response.json({ status: "ok" }); }, }, ], }, }); ``` Endpoint paths are exact and cannot contain parameters, wildcards, query strings, or hashes. They run before ordinary request context and routing, so keep endpoint-specific authorization in the handler. Responses still pass through `runtime.after`, and failures reach `runtime.error`. ## Short-circuit a request Return a `Response` from `runtime.before` to skip Farm's route handler. The response still passes through every `runtime.after` hook. ```ts export const maintenancePlugin = definePlugin({ name: "acme:maintenance", runtime: { before({ request }) { const url = new URL(request.url); if (process.env.MAINTENANCE === "1" && !url.pathname.startsWith("/health")) { return Response.json({ error: "Service temporarily unavailable" }, { status: 503 }); } }, }, }); ``` Authorization plugins may reject requests here, but handlers should still enforce resource-level permissions close to protected data. ## Background work and cancellation Every runtime event includes the request `signal` and `waitUntil()`. ```ts runtime: { after({ request, response, durationMs, signal, waitUntil }) { if (!signal.aborted) { waitUntil( metrics.write({ pathname: new URL(request.url).pathname, status: response.status, durationMs, }), ); } }, }, ``` Do not use `waitUntil()` for work the response depends on. Await required writes in the hook itself. ## Observe routes The router group handles discovery and page-route matching. ```ts export const routeReportPlugin = definePlugin({ name: "acme:route-report", setup() { return { discovered: 0 }; }, router: { discovered(route, { state }) { state.discovered += 1; if (route.kind === "page" || route.kind === "layout") { console.log(route.kind, route.pattern); } else { console.log(route.kind, route.path); } }, generated(summary, { state }) { console.log("Route entries", state.discovered); console.log("Pages", summary.pageCount); }, after(result) { if (!result.matched) { console.log("Page miss", result.pathname); } }, }, }); ``` `discovered` receives page, layout, middleware, and API route records. Narrow with `route.kind` before reading kind-specific fields. ## Transform rendered HTML Use `render.before` for route-aware setup and `render.html` to return changed HTML. ```ts export const htmlMarkerPlugin = definePlugin({ name: "acme:html-marker", render: { html(html, render) { if (render.pathname.startsWith("/docs")) { return html.replace("</head>", '<meta name="docs-runtime" content="farm"></head>'); } }, }, }); ``` HTML transforms require Farm to buffer the rendered document before sending it. Keep streaming intact by using `render.before` when no HTML rewrite is required. ## Extend builds Build hooks receive the state returned by `setup`. ```ts export const deploymentReportPlugin = definePlugin({ name: "acme:deployment-report", setup() { return { startedAt: Date.now() }; }, build: { before(bundle, { state }) { console.log("Building", bundle.preset, state.startedAt); }, configure(nitroConfig) { return { ...nitroConfig, sourceMap: true, }; }, after(result) { console.log("Build complete", result.success); }, }, }); ``` `setup` can run in a build manager and again in a deployed runtime. Keep it deterministic and avoid assuming it represents one global process forever. ## Development hooks ```ts export const devInspectorPlugin = definePlugin({ name: "acme:dev-inspector", dev: { server(vite) { console.log("Dev server", vite.config.root); }, update(update) { console.log("Updated", update.file, update.modules); }, }, }); ``` Development hooks are not bundled into the production request lifecycle. ## Start and close resources Use `runtime.start` for runtime-only startup and `runtime.close` for plugin cleanup. Long-running Node output completes startup before listening and invokes close after active requests and streams drain. ```ts export const queuePlugin = definePlugin({ name: "acme:queue", setup() { return { client: createQueueClient() }; }, runtime: { async start({ state }) { await state.client.connect(); }, async close({ state, reason }) { await state.client.close(); console.log("Queue closed", reason); }, }, }); ``` Use `context.lifecycle.onShutdown()` when setup creates a resource that should be disposed independently of the plugin's structured runtime hooks: ```ts export const redisPlugin = definePlugin({ name: "acme:redis", setup({ lifecycle }) { const redis = createRedisClient(); lifecycle.onShutdown(() => redis.quit()); return { redis }; }, }); ``` Farm runs registered disposers once, in reverse registration order, after attempting every plugin shutdown hook. One failing hook does not prevent later hooks or resource disposers from running. Request-driven serverless and edge hosts may initialize lazily and may not expose a reliable shutdown event. Correctness must not depend solely on cleanup hooks there; use transactions, leases, and idempotent queue handling for externally visible work. ## Context rules - `runtime.context` must return a plain object or nothing. - Farm merges context from all plugins before `runtime.before`. - Duplicate top-level context keys fail the request and name both owners. - `ctx` is server-only and read-only at the top level. - `req` is the shared mutable request store. - Only `req.set(key, value, { exposeToPage: true })` exposes data to page props. - Returning a new `Request` preserves both `ctx` and `req` for later hooks. ## Legacy compatibility Flat hooks continue to work for existing plugins: | Legacy hook | Structured hook | | --------------------------------------------------- | -------------------------------------------------- | | `config` | `configure` | | `ready` / `shutdown` | `runtime.start` / `runtime.close` | | `beforeRequest` / `beforeApiHandler` | `runtime.before` | | `afterResponse` / `afterApiHandler` | `runtime.after` | | `routeDiscovered` / `routesGenerated` | `router.discovered` / `router.generated` | | `beforeRouteMatch` / `afterRouteMatch` | `router.before` / `router.after` | | `beforeRender` / `afterRender` | `render.before` / `render.html` | | `beforeBundle` / `beforeNitroBuild` / `afterBundle` | `build.before` / `build.configure` / `build.after` | | `devServerCreated` / `hmrUpdate` | `dev.server` / `dev.update` | Do not define both the legacy and structured form of the same phase in one plugin. Farm intentionally executes both for compatibility. ## Testing checklist - Verify `setup` state and `runtime.context` inference with a type test. - Test `pre`, normal, and `post` hook order. - Test transformed requests, responses, status codes, headers, and bodies. - Test a `runtime.before` short circuit and confirm `runtime.after` still runs. - Test context-key collisions and error-hook failures. - Test page HTML transforms separately from API responses. - Test client hydration, navigation, cancellation, browser errors, and reverse-order cleanup when the plugin has a client module. - Run the plugin against both the dev server and a production preset build. --- ## DevTools URL: /docs/plugins/devtools A development workspace for routes, APIs, integrations, runtime diagnostics, and syntax-highlighted browser module inspection. # DevTools `@farm.js/devtools` is Farm's official DevTools. It brings the runtime snapshot into a development-only workspace: browse routes and their runtime settings, inspect configured integrations, and compare application source with the JavaScript Vite served. The interface uses the Farm logo, locally bundled Geist fonts, light and dark themes, and syntax-highlighted code with copy controls. It replaces the deprecated built-in dashboard from [Built-in DevTools](/docs/devtools). ## Setup New projects scaffolded with `create @farm.js/app` already include the plugin. For an existing app: ```bash pnpm add -D @farm.js/devtools ``` ```ts title="farm.config.ts" import { defineConfig } from "@farm.js/core"; import { devtools } from "@farm.js/devtools"; export default defineConfig({ plugins: [devtools()], }); ``` Run `farm dev`, then use the **DevTools** button in the lower-left corner. The existing `Command + Shift + .` shortcut on macOS, or `Ctrl + Shift + .` on Windows and Linux, opens the same window. `Escape`, the close button, or clicking outside returns to the app. The launcher matches Hints' compact, rounded trigger, with a small Farm logo and a Geist Mono label. It follows your system's light or dark theme, independently of the window's theme choice. The window dims and blurs the app behind it. Its content scrolls independently, and closing it restores the app's scrolling and focus without jumping the page. Use the sun/moon button to switch themes. DevTools follows your system theme until you choose one, then remembers your choice. The launcher URL is `/__farm/devtools` on your development origin. The UI loads on demand in an iframe, isolated from application CSS. Its fonts, code highlighter, and assets are served locally, without CDN requests. The maintained `examples/hints-demo` combines DevTools with the separate Hints plugin. ## What you can inspect | View | Purpose | | ----------- | ------------------------------------------------------------------------------------------------------------------------------- | | Overview | Real route/API/integration counts and framework diagnostics. | | Routes | Filter pages, layouts, loading states, and error boundaries; inspect source paths and effective runtime, regions, and duration. | | API | Registered methods and paths, source modules, and server runtime controls. Selecting an endpoint never invokes it. | | Systems | Integration routes, middleware/provider/model counts, request middleware, and KV mount metadata. | | Runtime | Deployment target, Nitro preset, output directory, schedules, workflows, layers, feature switches, and environment key names. | | Inspect | Source and served JavaScript for already-transformed browser modules, with imports and importers. | | Diagnostics | Runtime configuration checks shared with `farm doctor`. These are not TypeScript or accessibility checks. | | Snapshot | The current JSON snapshot, with syntax highlighting and copy support. | Use **Refresh** to capture the latest workspace state. The footer pairs Hints-style **Development only** and **Esc to close** labels with the snapshot time and copy feedback. If the server becomes unavailable, DevTools identifies the stale snapshot instead of silently presenting it as current. Press `/` in Routes or API to focus its filter. ## Inspect source and generated code Open an application page before opening **Inspect**. Select a module to see its current source on disk alongside the most recent browser transform cached by Vite. For a TSX component, this makes removed TypeScript annotations, generated JSX calls, and rewritten imports visible. Code is highlighted as JavaScript/TypeScript/JSX; other source formats remain readable but do not have dedicated language grammars. Copy preserves the original text, not highlighting markup. This is a **source-to-output comparison**, not a per-plugin transform timeline or an exact diff. It does not instrument transform hooks, run modules, or trigger transformations just to populate the inspector. After an edit, reload the app and refresh Inspect so the served transform is current. Only known, already-transformed browser modules inside the project are eligible. Server-only modules, virtual modules, dependencies, hidden paths, and symlinks escaping the project are excluded. Supported source extensions are JS/TS (including JSX, TSX, MJS, CJS, MTS, and CTS), CSS, Vue, and Svelte. The list is capped at 500 modules. Source and generated output are each limited to 1 MiB; the display shows up to 1,500 lines, while Copy includes the full bounded text. ## Options Adding the plugin is the opt-in. There is no extra `enabled` flag. ```ts title="farm.config.ts" devtools({ launcher: false, // Keep keyboard access, hide the floating button. shortcut: "mod+shift+d", // Or false to disable keyboard access. inspect: false, // Omit the module inspector and its endpoints. }); ``` All options are optional and typed. `launcher` and `inspect` default to `true`. If `shortcut` is omitted, Farm's existing shortcut configuration is preserved. Register one DevTools instance. Without this plugin, apps fall back to the deprecated built-in dashboard; the top-level `devtools` configuration keeps working during the transition. Remove `devtools: false` or `devtools: { enabled: false }` before adding `devtools()`; contradictory settings fail with an actionable configuration error. ## Security and production The plugin removes itself during production configuration. Its UI, launcher, module endpoints, fonts, and syntax highlighter are not emitted into the application's production output. No renderer-specific adapter is required. DevTools is read-only, but source code and route metadata are sensitive. Keep your development server on a trusted network. Browser requests must use a trusted development host and cannot come from a different origin. Custom development hostnames must be explicitly listed in Vite's `server.allowedHosts` (or set as `server.host`). Wildcard `allowedHosts: true` does not disable this plugin's host checks. Snapshots contain environment **key names only**, not environment values, credentials, request bodies, cookies, or application records. Browser source can still contain literals you wrote into client code; the inspector does not claim to redact source code. The read-only snapshot endpoint and `farm doctor` retain their existing behavior. See [Built-in DevTools](/docs/devtools) for the core dashboard, [Doctor](/docs/doctor) for terminal checks, and [Hints](/docs/plugins/hints) for opt-in accessibility and performance checks. --- ## Federation Plugin URL: /docs/plugins/federation Publish and load independently deployed browser modules while Farm keeps server code and credentials isolated. # Federation Plugin `@farm.js/federation` lets one Farm application publish browser modules and another load them at runtime. It fits independently deployed product areas, gradual migrations, and UI owned by separate teams when a shared package and one release pipeline are no longer enough. ![A Farm host loading an independently deployed checkout module after hydration](/federation-demo.png) The initial release is browser-only. Pages can load remote components after hydration, but Server Components, server functions, server queries, API handlers, environment variables, and credentials remain owned by the application that defines them. [RFC #885](https://github.com/farming-labs/farm.js/issues/885) tracks a separate server-federation design with explicit trust and isolation boundaries. ## Install Install the package in every producer and host: ```bash pnpm add @farm.js/federation ``` ## Publish a module The producer exposes local browser modules from `farm.config.ts`: ```ts import { defineConfig } from "@farm.js/core"; import { federation } from "@farm.js/federation"; export default defineConfig({ plugins: [ federation({ name: "checkout", exposes: { "./UpgradeCard": "./src/components/upgrade-card.tsx", }, publicPath: "https://checkout.example.com/", }), ], routeRules: { "/**": { cors: true, headers: { "Cross-Origin-Resource-Policy": "cross-origin", }, }, }, }); ``` A production build emits `mf-manifest.json`, a remote entry, and the chunks required by exposed modules. `publicPath` must be the public producer origin or base URL, not an internal build URL. Cross-origin hosts also need permission to read the manifest and chunks. The `routeRules` above are appropriate for a producer whose public assets are intentionally available to other origins. ## Configure a host The host names each producer and points to its public manifest: ```ts import { defineConfig } from "@farm.js/core"; import { federation } from "@farm.js/federation"; export default defineConfig({ plugins: [ federation({ name: "storefront", remotes: { checkout: { entry: "https://checkout.example.com/mf-manifest.json", }, }, }), ], }); ``` Use one `federation()` call per application so names, shared packages, and runtime initialization have one authoritative configuration. ## Load after hydration Remote browser code cannot render during SSR. Load it from a client component effect or an event handler and render an explicit loading and failure state: ```tsx "use client"; import { loadRemote } from "@farm.js/federation/client"; import { useEffect, useState, type ComponentType } from "react"; interface UpgradeCardProps { workspace: string; onUpgrade?: (seats: number) => void; } interface UpgradeCardModule { default: ComponentType<UpgradeCardProps>; } export function CheckoutSlot() { const [RemoteCard, setRemoteCard] = useState<ComponentType<UpgradeCardProps>>(); const [error, setError] = useState<string>(); useEffect(() => { let active = true; void loadRemote<UpgradeCardModule>("checkout/UpgradeCard") .then((module) => { if (active) setRemoteCard(() => module.default); }) .catch((cause: unknown) => { if (active) setError(cause instanceof Error ? cause.message : "Checkout did not load"); }); return () => { active = false; }; }, []); if (error) return <p role="alert">{error}</p>; if (!RemoteCard) return <p aria-live="polite">Loading checkout...</p>; return <RemoteCard workspace="Acme" />; } ``` The generic passed to `loadRemote<T>()` types the module used by the host. With `types: true`, which is the default, producers generate declarations and hosts consume declarations supported by Module Federation. Set `types: false` when builds must stay independent from a reachable producer, then keep the small host-side module contract as shown above. Preload a producer before an expected interaction: ```ts import { preloadRemote } from "@farm.js/federation/client"; void preloadRemote("checkout"); ``` Preloading is a performance hint. The UI still needs a loading state and a retry path because an independently deployed producer can be unavailable or incompatible. ## Shared dependencies Farm shares the active renderer and its browser runtime as singletons by default. This prevents a remote React component, for example, from creating a second React runtime. The same behavior follows the configured Preact, Vue, Solid, or Svelte renderer. Add application-level singleton packages when both deployments must use the same live instance: ```ts federation({ name: "storefront", shared: { packages: { "@acme/design-system": { singleton: true, requiredVersion: "^2.0.0", }, "date-fns": true, }, }, }); ``` Use `shared: false` only for renderer-free browser modules or when the application intentionally owns every shared-package rule. ## Options | Option | Default | Purpose | | --------------- | -------------------------------- | ------------------------------------------------------------------ | | `name` | required | Stable producer or host name. | | `exposes` | `{}` | Public module names mapped to local browser files. | | `remotes` | `{}` | Producer aliases mapped to manifests or remote entries. | | `shared` | active renderer | Renderer and application packages reused across deployments. | | `manifest` | `true` when `exposes` is present | Emit `mf-manifest.json`. | | `filename` | `"remoteEntry.js"` | Logical remote-entry filename before Farm content hashing. | | `publicPath` | build base | Public origin or base URL recorded for producer assets. | | `types` | `true` | Generate producer declarations and consume remote declarations. | | `dev.remoteHmr` | `false` | Enable cross-application HMR or use `"full-reload"` while editing. | Remote entries may use absolute `http` or `https` URLs, root-relative URLs, or the standard Module Federation `name@URL` string form. Farm applies the same protocol, credential, and network-path validation to every form. Put authentication at the API boundary rather than embedding secrets in a browser manifest URL. ## Deployment and CSS Producer and host are separate deployable applications. Deploy the producer first, keep its public manifest URL stable, and verify the host against the production build. A host release can otherwise point at chunks that have already been removed by a producer deployment, so retain old immutable assets for the compatibility window used by your releases. CSS imported by an exposed module enters the host document. Scope producer styles to the exposed component or use a shared design system. Broad selectors such as `body`, `button`, or `*` can change the host application even when JavaScript module boundaries are correct. ## Security boundary A remote browser module is executable code with the same browser authority as the host UI. It can read data available to that page, make requests as the signed-in user, and interact with the DOM. Only load deployments and origins you trust. Federation is not a sandbox, and CORS does not make an untrusted remote safe. Keep secrets, provider SDK credentials, authorization checks, server functions, and API handlers on the server. A remote component should call an authenticated public API just like any other browser code. The host must not try to replace provider environment variables or treat the remote UI as a trusted server caller. ## Runnable example The [federation demo](https://github.com/farming-labs/farm.js/tree/main/examples/federation-demo) contains separate producer and host applications, a typed React component contract, loading and retry states, cross-origin production headers, and a callback from the remote UI to its host. --- ## Health Plugin URL: /docs/plugins/health Expose dependency-aware liveness, readiness, and startup probes for Farm applications and hosting platforms. # Health Plugin `@farm.js/health` gives a hosting platform three small HTTP probes and adds application dependency checks to readiness. It is designed for containers, Kubernetes, load balancers, and external uptime monitors that need to decide whether a Farm instance should receive traffic. Farm core already exposes process-level liveness and readiness at `/_farm/health/live` and `/_farm/health/ready`. Use those built-in endpoints when process startup and graceful shutdown are the only signals you need. Add this plugin when readiness also depends on resources such as a database, cache, queue, or required startup work. ## Install ```bash pnpm add @farm.js/health ``` ## Add health checks Register `health()` in `farm.config.ts`. A function is a required check. Use the object form to make a check optional or give it a shorter timeout. ```ts import { defineConfig } from "@farm.js/core"; import { health } from "@farm.js/health"; import { database, redis } from "./src/server/services"; export default defineConfig({ plugins: [ health({ checks: { database: () => database.execute("select 1"), cache: { check: () => redis.ping(), required: false, timeout: "500ms", }, }, }), ], }); ``` The plugin serves these paths by default: | Endpoint | Meaning | Failure behavior | | --------------------- | -------------------------------------------------------------------------------------- | ------------------------------------- | | `GET /health/live` | The Farm runtime can answer HTTP requests. It does not call external dependencies. | `503` during shutdown or after close. | | `GET /health/ready` | Required startup work and all current required dependency checks pass. | `503` removes the instance from use. | | `GET /health/startup` | Required startup checks have passed at least once during the current process lifetime. | `503` while startup is incomplete. | `HEAD` has the same status and headers without a body. Other methods return `405` with `Allow: GET, HEAD`. Every response uses `Cache-Control: no-store` so a proxy cannot reuse a stale health result. The plugin does not poll itself. Your hosting platform or monitor sends requests to these URLs and decides what to do with the status code. Health uses exact, route-scoped runtime endpoints. Ordinary pages, APIs, actions, and assets do not run the checks, and installing the plugin does not make otherwise static pages dynamic. ## Check behavior Checks run concurrently and get their own timeout. Returning `false`, throwing, or exceeding the timeout marks a check unavailable. Any other fulfilled result passes, so provider methods such as `ping()` can be returned directly. - A required failure makes the whole probe `unavailable` and returns `503`. - An optional failure makes the probe `degraded` and still returns `200`. - An `ok` probe returns `200`. Checks receive an abort signal. Pass it to SDKs that support cancellation so timed-out work does not continue in the background: ```ts health({ timeout: "1s", checks: { catalog: ({ signal }) => fetch("https://catalog.internal/status", { signal, headers: { accept: "application/json" }, }).then((response) => response.ok), }, }); ``` Keep readiness checks cheap and shallow. Check the dependency needed to serve requests, not every service reachable from it. Never put a database or provider check in liveness because a shared dependency outage should stop traffic, not restart every healthy application process. ## Startup checks Use `startupChecks` for work that must finish once before an instance receives traffic, such as confirming migrations or loading a required local model. ```ts health({ startupChecks: { migrations: () => migrationState.isCurrent(), searchModel: ({ signal }) => searchModel.waitUntilLoaded({ signal }), }, checks: { database: () => database.execute("select 1"), }, }); ``` Farm retries failed startup checks when `/health/startup` or `/health/ready` is requested. Once all required startup checks pass, that result is latched for the lifetime of the runtime. Readiness then runs only the checks in `checks` on each request. ## Configure the hosting platform Installing the plugin does not change deployment traffic by itself. Configure the platform to call the endpoints. A Kubernetes container can use: ```yaml startupProbe: httpGet: path: /health/startup port: 3000 periodSeconds: 2 failureThreshold: 30 readinessProbe: httpGet: path: /health/ready port: 3000 periodSeconds: 5 failureThreshold: 2 livenessProbe: httpGet: path: /health/live port: 3000 periodSeconds: 10 failureThreshold: 3 ``` Set the platform's probe timeout above the longest configured check timeout. The values above are a starting point, not universal production defaults. Tune periods and thresholds to the application's startup time and recovery behavior. When a platform accepts only one health URL, use `/health/ready`. It answers the most important traffic question: can this instance serve a real request now? A Docker image can use the same URL in its `HEALTHCHECK`, and an external monitor can use it for availability alerts. Request-driven serverless and edge platforms generally create and remove isolated invocations themselves rather than polling a long-lived process. The endpoints can still support external monitoring, but they do not control provider routing unless that provider explicitly supports an HTTP health setting. ## Response details and security Responses expose only the overall status by default: ```json { "status": "unavailable" } ``` Set `details: true` to include check names, status, whether they are required, probe type, and duration. Raw errors, messages, connection strings, provider responses, and stack traces are never included. ```json { "status": "degraded", "checks": { "database": { "status": "ok", "required": true, "probe": "readiness", "durationMs": 4 }, "cache": { "status": "unavailable", "required": false, "probe": "readiness", "durationMs": 501 } } } ``` Check names can still reveal infrastructure. Keep details disabled on a public endpoint, or use a request predicate to disclose them only to an authenticated monitor: ```ts health({ details(request) { const token = process.env.HEALTH_DETAILS_TOKEN; return Boolean(token) && request.headers.get("authorization") === `Bearer ${token}`; }, checks: { database: () => database.execute("select 1"), }, }); ``` The predicate controls response detail, not access to the probe or its status code. ## Paths and base paths Customize the application-relative paths when a platform expects a particular convention: ```ts health({ paths: { liveness: "/status/live", readiness: "/status/ready", startup: "/status/startup", }, }); ``` Farm automatically prefixes the configured `basePath`. With `basePath: "/store"`, the default readiness URL is `/store/health/ready`. Configure the hosting platform with the final public path. Health paths must be distinct safe absolute paths. They are reserved by the plugin's runtime endpoints, so do not create application pages or API routes at the same locations. The plugin rejects a collision with core's `server.health` paths because core owns those handlers before plugin endpoints run. ## Options | Option | Default | Purpose | | --------------- | -------------------------------------------------- | ------------------------------------------------------------- | | `checks` | `{}` | Named checks evaluated for every readiness request. | | `startupChecks` | `{}` | Named checks retried until required startup work passes once. | | `timeout` | `"1s"` | Default timeout for each check. | | `details` | `false` | Safe response details or a request predicate that enables it. | | `paths` | `/health/live`, `/health/ready`, `/health/startup` | Application-relative endpoint paths. | Numbers passed as timeouts are milliseconds. Duration strings support `ms`, `s`, `m`, and `h`. --- ## Hints Plugin URL: /docs/plugins/hints Find accessibility, performance, HTML, and third-party script problems while developing a Farm app. # Hints Plugin `@farm.js/hints` turns browser diagnostics into specific, route-aware fixes while you develop. It checks the page after hydration, after Farm client navigation, and after meaningful DOM changes. The result appears in a compact overlay beside the application instead of being buried across different browser panels. ## Install ```bash pnpm add -D @farm.js/hints ``` ## Start with every check Add `hints()` to `farm.config.ts`: ```ts import { defineConfig } from "@farm.js/core"; import { hints } from "@farm.js/hints"; export default defineConfig({ plugins: [hints()], }); ``` This enables the useful defaults: | Check | What it reports | | ---------------- | ----------------------------------------------------------------------------------------------------- | | Accessibility | WCAG AA violations from `axe-core`, including the matching selector, markup, and remediation link. | | Performance | LCP, CLS, INP, Farm hydration and navigation time, plus images that can cause layout shift. | | HTML | Missing document language, duplicate IDs, missing doctype, and nested interactive controls. | | Third-party code | External scripts, parser-blocking scripts, and script resources that cross the configured slow limit. | Select a finding to see its explanation and source snippet. When the finding has a selector, Hints scrolls the matching element into view and outlines it briefly. Category filters keep a busy page manageable. The overlay uses the Farm.js logo and header with neutral light and dark backgrounds, follows the system color scheme, and starts collapsed on clean routes. ## Tune the checks Every check accepts `false`, `true`, or focused options where thresholds are useful: ```ts hints({ accessibility: { level: "AA", impact: "moderate", exclude: ["[data-visual-test]"], }, performance: { lcp: 2_500, cls: 0.1, inp: 200, hydration: 500, navigation: 1_000, }, html: true, thirdParty: { slow: 1_000, allow: ["trusted.example", "https://static.example.com"], }, maxIssues: 50, }); ``` Performance time values are milliseconds. CLS is a unitless score. The limits control when a metric becomes a finding, while the current metric remains visible in the overlay. An allowed third party can be written as an exact origin or a hostname. Use axe rule overrides when a product has a deliberate exception: ```ts hints({ accessibility: { rules: { "color-contrast": { enabled: false }, }, }, }); ``` Prefer a narrow selector exclusion or rule override over disabling accessibility checks for the whole app. ## Overlay and console output Send findings to the overlay, browser console, or both: ```ts hints({ report: "both", overlay: { position: "bottom-left", open: "issues", }, }); ``` `open` accepts: - `"issues"`, the default, opens once when the route has findings. - `"always"` starts with the full panel visible. - `"collapsed"` starts with the small launcher visible even when findings exist. Use `report: "console"` for browser automation or a development environment where an overlay is not wanted. ## Development and production boundary Hints is a development diagnostic, not application runtime behavior. During a production build the plugin removes itself from Farm's resolved configuration before client and server bundles are generated. `axe-core`, Web Vitals, the overlay, and its Geist Mono font are therefore absent from production output. The scanner reads the live browser DOM. That makes it useful for UI produced after hydration and client navigation, but it is not a replacement for a source-level HTML validator or full manual accessibility testing. Some interaction states only exist after a user opens a menu, dialog, or other conditional interface. Hints rescans those states when their DOM changes. ## Runnable example The [hints demo](https://github.com/farming-labs/farm.js/tree/main/examples/hints-demo) intentionally contains accessibility, HTML, and layout-risk problems so every part of the inspection flow can be tested against real elements. --- ## API MCP URL: /docs/plugins/mcp Compose typed API routes, server functions, and standalone tools in one authenticated MCP server. # API MCP `@farm.js/mcp` composes selected Farm API routes, server functions, and standalone tools in one MCP server. Reuse an endpoint's or a server function's validation and middleware, or use `defineTool()` for an operation that doesn't need either. All of them share the same transport, tool catalog, and authorization policy. The endpoint defaults to `/api/mcp` and implements MCP Streamable HTTP. It is a server capability, unlike [WebMCP](/docs/plugins/webmcp), which registers tools in a supporting browser while a page is open. ## Install and configure ```bash pnpm add @farm.js/mcp ``` ```ts title="farm.config.ts" import { defineConfig } from "@farm.js/core"; export default defineConfig({ mcp: { authorize: async ({ request }) => { const session = await getSession(request); if (!session) return false; return { subject: session.user.id, scopes: session.scopes }; }, }, }); ``` The top-level config mounts the transport and owns its global authorization policy. Farm loads the installed `@farm.js/mcp` package only when this config is enabled. It never exposes an endpoint unless that endpoint opts in or is explicitly selected in `mcp.tools`. ## Declare tools in config You can keep MCP declarations in `farm.config.ts` and reference existing endpoint instances. The endpoint still owns its validation, middleware, and handler: ```ts title="src/app/api/projects/route.ts" import { createEndpoint } from "@farm.js/core/api"; import { requireProjectAccess, listProjects } from "../../../lib/projects"; export const GET = createEndpoint( "/api/projects", { method: "GET", middleware: [requireProjectAccess] }, ({ context }) => listProjects(context.session.user.id), ); ``` ```ts title="farm.config.ts" import { defineConfig } from "@farm.js/core"; import { defineTool } from "@farm.js/mcp"; import { z } from "zod"; import { GET as listProjects } from "./src/app/api/projects/route"; import { getSession } from "./src/lib/session"; import { searchProjects } from "./src/lib/projects"; export default defineConfig({ mcp: { name: "project-tools", tools: [ { endpoint: listProjects, name: "list_projects", description: "List projects visible to the current user.", }, defineTool({ name: "search_projects", description: "Search projects visible to the current user.", inputSchema: z.object({ query: z.string().trim().min(1) }), readOnlyHint: true, destructiveHint: false, execute: ({ query }, { authorization, signal }) => searchProjects({ query, userId: authorization.subject, signal }), }), ], authorize: async ({ request, tools }) => { const session = await getSession(request); if (!session) return false; return { subject: session.user.id, tools: tools .filter( (tool) => ["list_projects", "search_projects"].includes(tool.name) && session.scopes.includes("projects:read"), ) .map((tool) => tool.name), }; }, }, }); ``` `mcp.tools` is an explicit allowlist: **only those tools** are exposed, even if other endpoints have route-owned `mcp` metadata. Omit it to discover route-owned declarations instead. A bare reference such as `tools: [listProjects]` preserves that endpoint's metadata or derives its tool name. The object form overrides metadata without changing the underlying API route. An empty configured list, duplicate endpoint selection, or a missing mounted route fails the build. Config references require `createEndpoint("/api/...", options, handler)` with an explicit path matching the mounted route. Farm resolves the reference by method and path, not JavaScript object identity, because config and route modules can be loaded independently. A `createRouteFactory()` definition can also be passed, provided its endpoint is mounted as an app route. This does not mount new HTTP routes or expose plugin-owned routes. For endpoints whose path is inferred from their file, use the route-owned form below instead. Keep endpoint imports server-only; never import this config or route module into a browser client. ## Standalone tools Import `defineTool` from `@farm.js/mcp` and put its result in `mcp.tools`, alongside endpoint references or by itself. A standalone tool does not need `createEndpoint()`, a method, or a path. For example, `search_projects` above is callable through `/api/mcp`, but Farm does not create an `/api/search_projects` route. A server with only standalone tools works without any app API routes. Each definition requires a unique `name`, an object `inputSchema`, and an `execute` function. Use `z.object({})` for a tool with no arguments. Zod 4 works directly; other Standard Schema validators must also expose `~standard.jsonSchema.input()` for discovery. Unsupported schemas fail at startup/build rather than advertising an unusable tool. Standalone arguments are the schema's object directly, with no `body` or `query` wrapper: ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "search_projects", "arguments": { "query": "farm" } } } ``` Farm advertises the schema's input shape and runs the original validator before `execute`, including async refinements, defaults, and transforms. The first argument is typed as the **validated output**, not the unparsed wire input. The second argument provides: - `authorization`: the principal returned by `mcp.authorize`, including your custom claims. Public servers receive `{ subject: "anonymous" }`. - `request`: the incoming MCP Web Request, including credentials. - `signal`: cancellation from the request or MCP call. Pass it into cancellable work; cancellation cannot undo side effects that already happened. Return JSON-serializable application data, not a `Response`, stream, or MCP protocol envelope. Farm returns its JSON as text content and as `structuredContent: { result: data }`, matching endpoint-backed tools. Validation and execution failures become tool errors. Input that fails the schema returns the validator's message. A thrown `EndpointFailure` or a server function's declared failure returns `{ "error": { "code", "status", "message", "data" } }`. Any other error, including one thrown inside a refinement or transform, returns `Farm MCP tool execution failed.` and is logged on the server, so an exception message never reaches the agent. Only authorized tools can reach validation or execution. Keep definitions and their provider SDKs server-only. Use a shared database or storage backend for data read by both routes and standalone tools; config and route modules can be loaded independently, and module-local state is not a shared store. ## Server functions and queries A `createServerFn` or `createServerQuery` function can be a tool too, so the operation your forms and components call is the same one an agent calls. Give it `mcp` metadata with a `name`, then list it in `mcp.tools`: ```ts // src/features/contact/server.ts import { createServerFn } from "@farm.js/core/server-fn"; import { createServerQuery } from "@farm.js/core/server-query"; import { z } from "zod"; export const submitContact = createServerFn({ input: z.object({ email: z.string().email(), message: z.string().min(1) }), middleware: [requireAccount], mcp: { name: "contact_submit", description: "Send a message to the sales team" }, handler: ({ input, context }) => createTicket(context.account, input), }); export const listPlans = createServerQuery({ key: () => ["plans"], mcp: { name: "list_plans", description: "List the available plans" }, handler: () => db.plans.findMany(), }); ``` ```ts // farm.config.ts import { submitContact, listPlans } from "./src/features/contact/server"; export default defineConfig({ mcp: { authorize: authorizeAgent, tools: [submitContact, listPlans], }, }); ``` A tool call runs the function's own pipeline once: input schema, middleware, handler, output schema, and cache invalidations. The MCP request is the function's `request`, so middleware reads the agent's credentials from it the way it reads a browser's cookies. Arguments are the input schema's object directly; a function without an input schema takes `{}`. - Queries are advertised with `readOnlyHint: true` and return their data, not the client cache envelope. - Other server functions get the conservative defaults of a standalone tool (`destructiveHint: true`). Set the hints in `mcp` when the function is safe to repeat or only reads. - Input that fails the schema returns the validator's message, which describes the agent's own arguments. - A failure declared in `errors` returns `{ "error": { "code", "status", "message", "data" } }`, and `redirect()` returns `{ "redirect": { "url", "status" } }`, both as tool errors. - Any other error returns `Server function failed.`, the same sanitized message the browser gets. That includes an exception thrown inside an input refinement or transform. The error itself is logged on the server. Only functions listed in `mcp.tools` are exposed, and only through `authorize`. Adding `mcp` metadata alone publishes nothing, and functions keep working for your UI either way. ## Resources and prompts Tools let an agent act. Resources give it read-only context to load, and prompts are reusable message templates a client can offer its user. Declare both next to `tools`: ```ts title="farm.config.ts" import { defineConfig } from "@farm.js/core"; import { definePrompt, defineResource } from "@farm.js/mcp"; import { z } from "zod"; import { getNote, listNotes } from "./src/lib/notes"; export default defineConfig({ mcp: { authorize: authorizeAgent, resources: [ defineResource({ name: "handbook", uri: "docs://handbook", mimeType: "text/markdown", read: () => "# Handbook\n\nHow the team works.", }), defineResource({ name: "note", uriTemplate: "notes://{id}", list: async ({ authorization }) => (await listNotes(authorization.subject)).map((note) => ({ uri: `notes://${note.id}`, name: note.title, })), read: async ({ variables, authorization }) => { const note = await getNote(String(variables.id), authorization.subject); return note ? { json: note } : null; }, }), ], prompts: [ definePrompt({ name: "summarize_note", description: "Summarize one note.", arguments: z.object({ id: z.string(), tone: z.string().default("plain") }), render: ({ id, tone }) => `Summarize note ${id} in a ${tone} tone.`, }), ], }, }); ``` A resource has a fixed absolute `uri` or an RFC 6570 `uriTemplate`. A template's `list` returns the concrete URIs to show in `resources/list`; leave it out to list none. `read` receives the requested `uri`, the template `variables`, and the same `authorization`, `request`, and `signal` as a standalone tool. It returns content in one of these shapes: - A string, sent with the resource's `mimeType` (default `text/plain`). - `{ text, mimeType }`. - `{ json }`, sent as `application/json`. - `{ blob, mimeType }` with a `Uint8Array` or `ArrayBuffer`, sent base64-encoded. `read` can also return a list of these, or `null` to answer "resource not found". A prompt's `arguments` is an object schema of strings, since MCP sends prompt arguments as strings. Its JSON Schema lists the arguments to clients, and the schema itself validates them before `render` runs, with its defaults and transforms. `render` returns the prompt text, sent as one user message, or a list of `{ role, text }` messages. Resources and prompts run behind the same `authorize` as tools. An error thrown inside `read`, `list`, or `render` is logged on the server and answered with a generic message, so private details never reach the client. A server can offer only resources and prompts, with no tools. ## Validate tool results Add `outputSchema` to a standalone tool to check its returned data before sending it to the client. The handler's return type must match the schema's **input** type. Farm runs the original output validator once, including async refinements, defaults, and transforms, and advertises the schema's **output** shape in MCP discovery. ```ts import { defineTool } from "@farm.js/mcp"; import { z } from "zod"; const countCharacters = defineTool({ name: "count_characters", inputSchema: z.object({ text: z.string() }), outputSchema: z.object({ count: z.number().int().nonnegative() }), execute: ({ text }) => ({ count: Array.from(text).length }), }); // Include countCharacters in mcp.tools. ``` `outputSchema` describes your returned data, not the MCP envelope. A result of `{ count: 4 }` still produces JSON text content and `structuredContent: { result: { count: 4 } }`. Farm wraps the advertised schema to match that existing envelope. Objects, arrays, scalars, and `null` work when they are JSON-compatible. The SDK also checks the serialized result against the advertised schema. An invalid result becomes a tool error, without successful structured data; standalone output-validator messages are not exposed to the client. For endpoint-backed tools, Farm derives the MCP output schema from the existing `createRouteFactory()` **`output`** option. There is no second MCP-specific validator to maintain: ```ts title="src/app/api/health/route.ts" import { createRouteFactory } from "@farm.js/core/api"; import { z } from "zod"; const route = createRouteFactory(); export const GET = route.get("/api/health", { mcp: { name: "health", readOnlyHint: true }, output: z.object({ ok: z.boolean() }), handler: () => ({ ok: true }), }).endpoint; ``` The endpoint runtime validates and transforms plain handler results; MCP does not run that validator a second time. Raw `Response` results bypass the endpoint's output parser, but their successful JSON payload must still match the advertised MCP output shape. Non-success HTTP responses remain tool errors rather than being checked against a success schema. `createEndpoint()` does not have an `output` option; use the route factory when you need an endpoint output validator. Both forms are optional. Tools without an output schema retain their current behavior and do not advertise one. Zod 4 schemas must have a JSON-representable output; for example, pipe a transform into a representable schema with `.transform(Number).pipe(z.number())`. Other Standard Schema validators must expose `~standard.jsonSchema.output()` as well as their validation method. An output schema that cannot be represented fails registration/build instead of silently omitting the contract. Output validation cannot undo side effects already performed by the handler. ## Authorize individual tools Every `authorize` call receives: - `request`: the incoming Web Request, including its credentials. - `server`: the resolved MCP `name`, `version`, and endpoint `path`. - `tools`: the complete, read-only configured catalog. Each entry has its stable MCP `name`, `kind` (`"endpoint"` or `"standalone"`), optional title/description, and behavior hints. Endpoint entries also have HTTP `method` and route `path`; standalone entries do not. The catalog contains no handlers or validators. - `tool`: the untrusted requested name for a `tools/call`, if present. It is omitted for initialization and listing. For a JSON-RPC batch, Farm calls `authorize` for every item with that item's tool name and rejects the entire batch before dispatch when any item is denied; compare it against the catalog rather than trusting it. - `resource` and `prompt`: the untrusted requested URI for a `resources/read` and name for a `prompts/get`, the same way. Return `false` to reject the entire request with HTTP 401. Otherwise return a `subject` and, optionally, `tools`, an array of allowed **tool names**. Omit this return field to retain access to all configured tools; return `tools: []` to allow the MCP connection but no tools. Unknown names or malformed permission lists fail closed with a server error. `resources` and `prompts` work the same way for resource and prompt names: omit them to allow all, or return `[]` to allow none. Every allowed item in a batch must resolve to the same subject, scopes, and allowed tool names because one HTTP batch has one authenticated identity. Authorization runs once per batch item, so keep batches bounded when the policy performs remote or otherwise expensive session checks. Only allowed tools appear in discovery, and guessing a hidden name does not make it callable. Permissions are evaluated for each request and do not carry over between callers. A stable explicit `name`, such as `list_projects`, is the identifier to use in policy rules. `scopes` are authentication metadata; Farm does not automatically translate them into permissions. Your policy must map them to allowed names, as above. Likewise, `readOnlyHint` and other annotations are descriptive, not security rules. For row-level or argument-dependent checks, keep authorization in endpoint middleware or a standalone tool's `execute` function. Standalone tools receive the returned principal as `authorization`; endpoint-backed tools continue to authenticate through their existing middleware using forwarded credentials. The principal is not injected into an endpoint's context. This config does not implement an OAuth authorization server or issue tokens. ### OAuth discovery A rejected request's 401 carries `WWW-Authenticate: Bearer`. When your tokens come from an OAuth authorization server, set `oauth` so MCP clients can find it and sign the user in, as the [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) describes: ```ts title="farm.config.ts" export default defineConfig({ mcp: { authorize: verifyAccessToken, oauth: { authorizationServers: ["https://auth.example.com"], scopes: ["projects:read", "projects:write"], }, }, }); ``` The 401 then links the endpoint's [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) metadata: ```http WWW-Authenticate: Bearer resource_metadata="https://app.example.com/.well-known/oauth-protected-resource/api/mcp", scope="projects:read projects:write" ``` Farm serves that metadata, naming the endpoint as the `resource` and listing the authorization servers and scopes. It is built from the request, so it names the endpoint URL the client used. `authorize` still validates every token, including its audience; `oauth` only advertises where tokens come from. With a `basePath`, the metadata URL sits at the origin root, as RFC 9728 places it, so the proxy in front of the app must forward `/.well-known/oauth-protected-resource/*` to it. ## Keep the API route authoritative Define validation and endpoint authorization on the route as usual: ```ts title="src/app/api/projects/route.ts" import { createEndpoint, type EndpointMiddlewareContext } from "@farm.js/core/api"; import { z } from "zod"; async function requireProjectAccess({ request }: EndpointMiddlewareContext) { const session = await getSession(request); if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 }); return { session }; } export const GET = createEndpoint( { method: "GET", mcp: { name: "list_projects", description: "List projects visible to the current user.", readOnlyHint: true, }, query: z.object({ status: z.enum(["active", "planned"]).optional() }), middleware: [requireProjectAccess], }, async ({ query, context }) => ({ projects: await listProjects(context.session.user.id, query.status), }), ); ``` Use `mcp: true` when the generated name is sufficient. Farm derives names from the method and path, such as `get_projects` for `GET /api/projects` and `get_projects_by_team` for `GET /api/projects/[team]`. Use an object to set `name`, `title`, `description`, or behavior hints. Duplicate or invalid explicit names fail the build. A path that cannot produce a valid derived name fails with an instruction to provide one explicitly. Duplicate tool names, unsupported schemas, or an empty tool set also fail the build instead of producing a partially working server. Package and plugin authors use the same route-owned metadata on `createRouteFactory()` routes: ```ts import { createRouteFactory } from "@farm.js/core/api"; import { z } from "zod"; const route = createRouteFactory(); export const projectsRoute = route.post("/api/projects", { mcp: { name: "create_project", description: "Create a project.", destructiveHint: false, }, input: { body: z.object({ name: z.string().min(1) }) }, handler: async (_request, { input }) => createProject(input.body), }); ``` Both route APIs keep the HTTP method and path next to the handler, so there is no separate `"METHOD /api/path"` MCP map to synchronize. An MCP call runs the endpoint validator, middleware, handler, and error mapping exactly once. A non-2xx response becomes a tool error with its status and body. An error the handler throws instead returns `Farm API tool invocation failed.` and is logged on the server. It also forwards the original request's cookies and authorization header, so the route sees the same credentials as the MCP boundary. Tool arguments cannot replace security-sensitive credentials or request-provenance headers such as `Host`, `Origin`, `Referer`, `Forwarded`, or `X-Forwarded-*`. Use both authorization layers: - `mcp.authorize` protects the MCP transport and can reject requests before protocol handling. - Endpoint middleware protects the underlying API route, including direct HTTP and typed local calls. Authorization is required by default. `allowUnauthenticated: true` is an explicit escape hatch for a deliberately public server. ## Generated tool inputs Farm maps route inputs into one MCP argument object: | Route input | Tool argument | Notes | | ----------- | ------------- | --------------------------------------------------------------------- | | Params | `params` | Includes dynamic, catch-all, and optional catch-all segments. | | Query | `query` | Preserves repeated query values as arrays. | | JSON body | `body` | Available for methods that accept a request body. | | Headers | `headers` | Credential and request-provenance headers are never agent-controlled. | For example, a tool call for `GET /api/projects/[team]` can send: ```json { "params": { "team": "framework" }, "query": { "status": ["active", "planned"] } } ``` The route's Standard Schema implementation must be convertible to JSON Schema. Zod 4 routes work without additional configuration. Selected routes currently need JSON responses; binary, multipart, and streaming responses are rejected as tool errors. ## Forms that are also tools When a page's form submits to an API route that is exposed as a tool, the page's Markdown mirror says so. Its [`?actions` view](/docs/markdown#describe-a-pages-forms) adds the tool to that form: ```json { "type": "form", "method": "POST", "action": "/api/contact", "fields": [{ "name": "email", "type": "email", "required": true }], "tool": { "name": "contact_submit", "endpoint": "/api/mcp" } } ``` An agent reading the page can then call `contact_submit` through MCP instead of posting the form. The match uses the form's method and action against the endpoints this server exposes, including a custom `api.baseURL`. Forms bound to server functions in a React Server Components app are not matched yet. ## Connect a client Point an MCP client at the deployed endpoint and supply credentials using that client's secure header configuration. For a direct protocol check in local development: ```bash curl http://localhost:3000/api/mcp \ -H 'Authorization: Bearer development-token' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ --data '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` Do not put production tokens in source, browser code, an MCP tool description, or a checked-in client configuration. ## Observe tool executions Subscribe through the existing [observability API](/docs/observability), not by inspecting MCP response bodies. Farm emits `mcp.tool.complete` once a registered tool callback finishes: ```ts title="farm.config.ts" export default defineConfig({ observability: { events: ["mcp.tool.complete"], onEvent(event) { if (event.type !== "mcp.tool.complete") return; console.log(event.tool, event.outcome, event.durationMs); }, }, // Keep your existing mcp.tools and mcp.authorize configuration. }); ``` The event contains the configured MCP `server` name, transport `route`, registered `tool` name, `outcome: "success" | "error"`, and callback duration in milliseconds, plus normal event and trace metadata. Endpoint failures, execution failures and output validation failures report `error` even when MCP responds with HTTP 200. An `isError` property inside returned application data is not an MCP error envelope. These are **tool callback outcomes**, not agent runs, HTTP request totals or proof that the client received a response. Authorization failures, hidden/unknown tools and inputs rejected by the MCP SDK before the callback do not emit completion events. Validation performed inside Farm's callback does emit an error outcome. Keep HTTP request telemetry alongside tool telemetry to understand coverage; do not infer successful tool execution from an HTTP status. No arguments, results, credentials, principal, raw request URL or error message are included in this event. Tool/server names and configured paths are application-authored metadata: keep secrets out of them. Logging/export is opt-in through observability; this event does not send anything to Farm Infra or enable anonymous framework telemetry. Exporters must remain non-blocking and use bounded post-response work where the host requires it. ## Options | Option | Default | Purpose | | ---------------------- | ------------- | ----------------------------------------------------------------------------------------------- | | `authorize` | required | Receive the tool catalog and server identity; return a subject and optional allowed tool names. | | `tools` | route opt-ins | Compose endpoint references, server functions, and standalone `defineTool()` definitions. | | `allowUnauthenticated` | `false` | Deliberately permit a public MCP endpoint instead of `authorize`. | | `oauth` | none | Advertise OAuth authorization servers and scopes through RFC 9728 metadata and the 401. | | `resources` | none | Read-only context from `defineResource()`, at a fixed URI or a URI template. | | `prompts` | none | Reusable prompt templates from `definePrompt()`. | | `path` | `/api/mcp` | Canonical Farm API path for the Streamable HTTP endpoint. | | `name` | `farm-api` | MCP server name returned during initialization. | | `version` | `1.0.0` | Application server version returned during initialization. | Tool metadata supports `name`, `title`, `description`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`. Annotations describe intent to the client; they do not enforce permissions or replace confirmation for consequential actions. ## Runtime support The MCP transport works in development and production on Farm's stable Node, Vercel, Cloudflare, and Netlify targets. Removing the top-level `mcp` config removes the endpoint and MCP runtime from the application bundle. The lower-level `apiMcp()` helper remains available for advanced manual plugin composition. It has the same options as top-level config, including mixed tool definitions and per-request permissions. Endpoint-backed tools keep their route's validation, middleware, and execution. ```ts import { apiMcp } from "@farm.js/mcp"; export default defineConfig({ plugins: [apiMcp({ authorize: authorizeAgent })], }); ``` Normal applications do not need this manual form. The runnable [`examples/api-mcp`](https://github.com/farming-labs/farm.js/tree/main/examples/api-mcp) shows two endpoint-backed tools and a standalone search tool sharing one authorization policy. --- ## MSW Plugin URL: /docs/plugins/msw Share Mock Service Worker handlers between Farm development SSR and browser requests without shipping mocks to production. # MSW Plugin `@farm.js/msw` loads one [Mock Service Worker](https://mswjs.io/) handler module in both sides of a Farm development server. It starts MSW's Node interceptor before SSR and server code run, then starts the browser service worker before hydration. This keeps mocked responses consistent when a request moves between a server-rendered page and client code. The plugin is development-only. Farm removes it from the resolved production configuration, so the MSW runtimes, handlers, and worker are not included in the production client or server output. ## Install and configure ```bash pnpm add -D @farm.js/msw ``` ```ts title="farm.config.ts" import { defineConfig } from "@farm.js/core"; import { msw } from "@farm.js/msw"; export default defineConfig({ plugins: [ msw({ handlers: "./src/mocks/handlers.ts", }), ], }); ``` `handlers` is resolved from the Farm app root. The module may use a named `handlers` export, which is recommended, or default-export the array. ## Define shared handlers ```ts title="src/mocks/handlers.ts" import { delay, http, HttpResponse } from "@farm.js/msw/handlers"; export const handlers = [ http.get("/api/products", async () => { await delay(200); return HttpResponse.json([ { id: "tractor", name: "Tractor" }, { id: "harvester", name: "Harvester" }, ]); }), ]; ``` The `/handlers` entry re-exports MSW's common `http`, `graphql`, `ws`, `HttpResponse`, `delay`, `bypass`, and `passthrough` APIs. No separate `msw` install is needed. Use an absolute request URL when server code calls an external service. Relative handlers remain useful for same-origin browser and Farm API requests. ```ts title="src/mocks/handlers.ts" import { http, HttpResponse } from "@farm.js/msw/handlers"; export const handlers = [ http.get("https://inventory.example.com/status", () => HttpResponse.json({ available: true })), ]; ``` ## What the plugin owns | Development surface | Behavior | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Server | Starts `setupServer()` before Farm handles requests, covering SSR, API routes, queries, actions, and other Node-side fetches. | | Browser | Serves MSW's worker under the configured Farm `basePath` and starts `setupWorker()` before hydration. | | HMR | Reloads the server handler list when the handler module or one of its imported modules changes. Browser changes use Vite's normal module update. | | Shutdown | Stops the Node interceptor and browser worker when the corresponding Farm runtime closes. | | Production | Removes the plugin before production client and server bundles are generated. | This is the value over wiring MSW manually: the app does not need separate browser and server bootstrap files, a copied worker in `public`, custom startup ordering, or cleanup and HMR glue. ## Options ```ts msw({ handlers: "./src/mocks/handlers.ts", browser: true, server: true, onUnhandledRequest: "bypass", }); ``` Adding `msw()` to `plugins` enables mocking in development. Remove it from the array to disable both runtimes. | Option | Default | Purpose | | -------------------- | ---------- | ------------------------------------------------------------------- | | `handlers` | Required | Root-relative module containing the handler array. | | `browser` | `true` | Start the browser service worker. | | `server` | `true` | Start the Node interceptor used by development SSR and server code. | | `onUnhandledRequest` | `"bypass"` | Bypass, warn about, or reject requests without a matching handler. | Use `onUnhandledRequest: "error"` when the mock environment should be completely deterministic. The default is `"bypass"` because a Farm development server also makes ordinary framework and asset requests that usually should continue normally. ## Choose one runtime Some apps need mocks on only one side: ```ts msw({ handlers: "./src/mocks/handlers.ts", browser: false, }); ``` With `browser: false`, SSR and server code are mocked but browser requests reach their normal destination. Set `server: false` for the inverse. Disabling the browser runtime also prevents the browser MSW entry from being added to the development client bundle. ## PWA compatibility `@farm.js/msw` and [`@farm.js/pwa`](/docs/plugins/pwa) own service workers in different modes. MSW runs only in development, while the PWA worker registers only in production. Their default workers therefore do not compete for the same Farm page. ## Scope and safety Mocks change development network behavior globally for the app. Keep handler modules free of secrets, and do not treat a mocked authorization response as an authorization check. Production routes must still validate sessions, permissions, input, and provider responses normally. The runnable [`examples/msw-demo`](https://github.com/farming-labs/farm.js/tree/main/examples/msw-demo) shows the same intentionally nonexistent endpoint being intercepted from a Farm API route and after browser hydration. --- ## Plugin Ecosystem URL: /docs/plugins Extend Farm's server and browser lifecycle with typed runtime, router, render, build, navigation, and development hooks. # Plugin Ecosystem Plugins are Farm's framework extension surface. Use a plugin to change how Farm configures an app, handles requests, discovers routes, renders HTML, builds output, observes browser hydration and navigation, or responds to development events. Use an [integration](/docs/integrations) for a product or service such as authentication, payments, email, analytics, or a database. Integrations own product config, endpoints, typed callers, and providers. Plugins own framework lifecycle behavior. ## Official plugins | Plugin | Purpose | | -------------------------------------- | ------------------------------------------------------------------------------------------------- | | [API MCP](/docs/plugins/mcp) | Expose opted-in typed API routes as authenticated Streamable HTTP tools. | | [Analyzer](/docs/plugins/analyzer) | Explain page, client, and server build size and enforce readable CI limits. | | [Content](/docs/plugins/content) | Validate local Markdown, MDX, JSON, and YAML as typed server collections. | | [DevTools](/docs/plugins/devtools) | Inspect routes, runtime configuration, diagnostics, and browser module output during development. | | [Federation](/docs/plugins/federation) | Publish and load independently deployed browser modules while server capabilities stay local. | | [Health](/docs/plugins/health) | Expose dependency-aware liveness, readiness, and startup probes to hosting platforms. | | [Hints](/docs/plugins/hints) | Find accessibility, performance, HTML, and third-party problems in the live development page. | | [MSW](/docs/plugins/msw) | Share request handlers across development SSR and the browser without shipping production mocks. | | [Partytown](/docs/plugins/partytown) | Move explicitly opted-in third-party scripts from the main thread into a web worker. | | [PWA](/docs/plugins/pwa) | Generate a route-aware service worker for offline navigation, caching, and safe updates. | | [Search](/docs/plugins/search) | Generate a chunked browser search index from emitted static pages. | | [Scripts](/docs/plugins/scripts) | Load external browser SDKs with typed handles, timing, consent, dependencies, and retries. | | [Sentry](/docs/plugins/sentry) | Report server errors with Farm event and route context, trace requests, and flush on shutdown. | | [StyleX](/docs/plugins/stylex) | Compile StyleX with pre-paint development styles, CSS HMR, and extracted production CSS. | | [WebAssembly](/docs/plugins/wasm) | Import prebuilt Wasm modules in browser code and module workers. | | [WebMCP](/docs/plugins/webmcp) | Expose explicit, typed browser tools to agents through the experimental WebMCP API. | Each published plugin has its own setup page in this section. Use [Create a Plugin](/docs/plugins/create-plugin) when the behavior is specific to your application or package. ## Community plugins Community plugins are maintained by their authors outside the Farm project. | Plugin | Purpose | | -------------------------- | ------------------------------------------------------------------------------------------ | | [Blyp](/docs/plugins/blyp) | Record request-scoped logs, propagate traces, and collect lifecycle and browser telemetry. | ## Plugin authoring Start with [Create a Plugin](/docs/plugins/create-plugin) for the server and build lifecycle. Add the [Client Plugin API](/docs/plugins/client) only when the plugin also needs browser hydration, navigation, error, performance, or cleanup hooks. Client plugins are an authoring capability, not a separately installed package. ## What framework-focused means A framework-focused plugin changes **how Farm operates**, rather than adding one application's business feature. It runs at stable framework boundaries and can be reused across unrelated applications. | Question | Usually choose | | ------------------------------------------------------------------------------------------- | -------------- | | Does it observe or transform every request, response, route, render, or build? | Plugin | | Does it wrap a product and expose config, endpoints, storage, providers, or a typed client? | Integration | | Is it request policy owned by one application or route tree? | Middleware | | Is it a shared application foundation containing config, routes, integrations, and plugins? | Layer | For example, request tracing is framework-focused because it should work for pages, APIs, actions, docs, and integrations without knowing their business logic. A Stripe checkout extension is product-focused because it owns Stripe config, webhook endpoints, and a typed API. ## What plugins can build The lifecycle is intentionally broad enough for infrastructure and tooling without turning plugins into product modules. | Category | Example plugin | | -------------------- | --------------------------------------------------------------------------------------------- | | Observability | Create trace IDs, record latency, export spans, or report route failures. | | Security | Apply CSP and security headers, enforce request policy, or attach nonces. | | Traffic | Implement global redirects, rewrites, compression, maintenance mode, or response tagging. | | Routing | Generate route manifests, enforce route conventions, or report conflicting patterns. | | Rendering | Inject metadata, transform final HTML, or measure server rendering. | | Browser runtime | Observe hydration, navigation, errors, performance entries, and cleanup. | | Deployment | Configure Nitro output, add platform metadata, or validate runtime capabilities. | | Development | Add a route inspector, HMR diagnostics, performance reporting, or custom dev-server behavior. | | Organization tooling | Package shared logging, security, and build policy for every company application. | A plugin can cover several lifecycle phases, but focused plugins are easier to order, test, and reuse. Product endpoints and typed product callers should remain in an integration even when that integration contributes a plugin internally. ## Register a plugin **farm.config.ts** ```ts import { defineConfig } from "@farm.js/core"; import { requestTracingPlugin } from "./src/plugins/request-tracing"; export default defineConfig({ plugins: [requestTracingPlugin()], }); ``` ## Interface `definePlugin()` infers private setup state and request context across the plugin. ```ts import { definePlugin } from "@farm.js/core"; export const frameworkPlugin = definePlugin({ name: "framework-plugin", enforce: "pre", configure(config) {}, setup({ env }) { return { startedAt: Date.now() }; }, runtime: { start({ state }) {}, context({ request, state, req }) { return { pathname: new URL(request.url).pathname }; }, before({ request, ctx, state }) {}, after({ response, ctx, state }) {}, error({ error, ctx, state }) {}, close({ reason, state }) {}, }, router: { discovered(route, { state }) {}, generated(routes, { state }) {}, before(route, { state }) {}, after(result, { state }) {}, }, render: { before(render, { state }) {}, html(html, render, { state }) {}, }, build: { before(bundle, { state }) {}, configure(buildConfig, { state }) {}, after(result, { state }) {}, }, dev: { server(viteServer, { state }) {}, update(update, { state }) {}, }, client: { public: { release: "2026.07" }, setup({ public: config }) { return { release: config.release }; }, navigation: { rendered({ state, to }) { console.log(state.release, to.pathname); }, }, }, }); ``` Only define the groups your plugin needs. | Surface | Purpose | | ----------- | ------------------------------------------------------------------------------- | | `configure` | Transform Farm config before the development or production pipeline is created. | | `setup` | Create private, typed plugin state for one plugin manager. | | `runtime` | Wrap Web `Request` and `Response` handling. | | `router` | Observe route discovery, generation, and matching. | | `render` | Observe rendering or transform final HTML. | | `build` | Run around bundling and configure the Nitro build. | | `dev` | Access the Vite server and HMR updates. | | `client` | Define browser setup, hydration, navigation, errors, performance, and cleanup. | ## Complete lifecycle reference ### Plugin definition | Property | Required | Behavior | | ----------- | -------- | -------------------------------------------------------------------------------------- | | `name` | Yes | Stable plugin identity. Package authors should namespace it, such as `acme:security`. | | `version` | No | Optional plugin version metadata for package authors and tooling. | | `enforce` | No | Places the plugin in the `pre` or `post` ordering group. Omit it for normal order. | | `configure` | No | Receives Farm config before pipeline creation and may return replacement config. | | `setup` | No | Runs once per plugin manager and returns private state inferred by every grouped hook. | | `runtime` | No | Handles portable Web `Request` and `Response` lifecycle work. | | `router` | No | Observes route discovery, route graph generation, and page matching. | | `render` | No | Observes page rendering and may transform final HTML. | | `build` | No | Observes bundling and may configure the Nitro build. | | `dev` | No | Accesses the Vite development server and HMR updates. | | `client` | No | Defines the browser lifecycle and explicitly public client data. | Both hooks receive Farm's plugin context. `setup` additionally receives the resolved `env`. Its returned state is private to that plugin instance and is not serialized to the browser. Framework `onError` dispatch invokes every selected reporter in plugin order and awaits all returned promises, even if another reporter throws or rejects. Reporters can run concurrently. After all settle, the first observed reporter failure is surfaced to the dispatch caller; startup and request handling report it separately from the original failure. Keep error reporters finite: they are awaited before the caller continues. Other parallel hooks remain fail-fast: a synchronous throw stops dispatch before later hooks are invoked and rejects with the same thrown value. Farm still observes promises from already-started hooks so their later rejections are handled. This does not wait for or cancel that work; asynchronous failures retain normal `Promise.all` behavior. In development, a native Vite restart keeps the same Farm plugin runtime and instrumentation alive; it does not rerun `setup` or dispose resources registered with `lifecycle.onShutdown()`. Awaiting the Farm dev server's `close()` waits for an in-progress restart, closes the current Vite server, and then awaits plugin cleanup and instrumentation shutdown exactly once. Cleanup errors reject `close()`. New restart requests are rejected once final shutdown begins. If initial development-server creation fails, Farm awaits cleanup of the resources it has created before rejecting: Vite's server once `configureServer` begins (even if a Vite hook fails before the server is returned), registered plugin shutdown hooks and disposers, and instrumentation. This includes failures in `configureServer`, its returned post hook, and middleware-mode `buildStart`. A failing `onError` hook or cleanup step is reported separately and does not replace the original startup error or skip the remaining cleanup. Development startup failures are sent to `onError` before cleanup: errors during creation use `phase: "createServer"`, while errors when listening (such as an occupied port) or running `runtime.start` use `phase: "startDevServer"`. A creation failure is not reported again by the startup wrapper. Async error hooks are awaited while plugin resources are still available; a failed error hook does not prevent cleanup or replace the startup error. ### Runtime hooks | Hook | Runs | May return | | ------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------- | | `runtime.start` | Once when the runtime manager starts. | Nothing. | | `runtime.endpoints` | On an exact application-relative path owned by the plugin. | A Web `Response`. | | `runtime.context` | At the start of every request. | A plain object merged into typed request `ctx`. | | `runtime.before` | Before Farm invokes the matched handler. | A replacement `Request`, a short-circuit `Response`, or nothing. | | `runtime.after` | After a handler or short circuit produces a response. | A replacement `Response` or nothing. | | `runtime.error` | When runtime context, before, handler, or after work throws. | Nothing. | | `runtime.close` | During graceful shutdown in long-running Node output. | Nothing. | Runtime request hooks apply to page, API, server-action, integration, docs, asset, and general requests. Use `runtime.endpoints` when a plugin owns a small exact URL and should not make unrelated static pages dynamic. Endpoint paths are application-relative, automatically honor `basePath`, and do not accept route parameters or wildcards. Direct `api` calls from [`createApiClients()`](/docs/api-client#server-callers) invoke an endpoint inside the current request; they do not create another plugin request lifecycle. Endpoint `middleware` still runs. Use endpoint middleware for authorization needed by both direct and HTTP callers. ### Runtime event values | Value | Available in | Meaning | | ------------- | --------------------------------------- | ----------------------------------------------------------------------- | | `request` | Endpoint, context, before, after, error | Current Web `Request`, including transformations from earlier plugins. | | `response` | After | Current Web `Response`, including transformations from earlier plugins. | | `state` | Every grouped hook | Private value returned by this plugin's `setup`. | | `ctx` | Before, after, error | Read-only merge of all plugin request-context results. | | `req` | Endpoint, context, before, after, error | Mutable request store shared with middleware and server rendering. | | `kind` | Runtime request hooks | Request category such as `endpoint`, `page`, `api`, or `action`. | | `route` | Runtime request hooks | Matched pathname, route pattern, and params when Farm has them. | | `path` | Endpoint | Normalized application-relative endpoint path registered by the plugin. | | `signal` | Runtime request hooks | Abort signal for cancelled or disconnected requests. | | `waitUntil()` | Runtime request hooks | Registers non-blocking work with hosts that support background tasks. | | `durationMs` | After, error | Elapsed request time at that lifecycle phase. | ### Router hooks | Hook | Purpose | | ------------------- | ------------------------------------------------------------------------- | | `router.discovered` | Observe each page, layout, middleware, or API route as Farm discovers it. | | `router.generated` | Inspect the completed page/layout route summary. | | `router.before` | Observe a pathname and method before page-route matching. | | `router.after` | Inspect the match result, params, route pattern, and layouts. | Router hooks are useful for diagnostics, conventions, manifests, and instrumentation. They should not silently replace application authorization or handler-level validation. ### Render hooks | Hook | Purpose | May return | | --------------- | ------------------------------------------------- | ---------------------------- | | `render.before` | Observe the matched page before rendering starts. | Nothing. | | `render.html` | Inspect or transform the completed HTML document. | Replacement HTML or nothing. | Adding `render.html` requires Farm to buffer the final document. Prefer `render.before` when observation is enough and preserving streaming matters. ### Build hooks | Hook | Purpose | May return | | ----------------- | --------------------------------------------------------------------------------- | ------------------------------------ | | `build.before` | Observe the root, preset, output paths, and universal-build mode before bundling. | Nothing. | | `build.configure` | Read or transform the Nitro build configuration. | Replacement build config or nothing. | | `build.after` | Observe build success and resolved output information. | Nothing. | ### Development hooks | Hook | Purpose | | ------------ | ------------------------------------------------------------ | | `dev.server` | Access the created Vite development server. | | `dev.update` | Observe changed files and invalidated module IDs during HMR. | Development hooks do not run as part of the deployed request lifecycle. ## Client lifecycle The optional `client` property keeps browser behavior under the same plugin identity and ordering as its server hooks. Farm extracts the known lifecycle hooks into its generated browser runtime without copying the server plugin, server setup state, or private environment values. ```ts client: { public: { release: "2026.07", }, setup({ public: config }) { return { release: config.release }; }, navigation: { rendered({ state, to }) { console.log(state.release, to.pathname); }, }, }, ``` Farm runs `client.setup` once, surrounds initial hydration, emits navigation phases from the active router, isolates hook failures, aborts superseded navigation sessions, and closes plugins during page hide or HMR disposal. Only JSON-safe values in `client.public` enter the browser bundle. Server setup state, request context, and private environment values never cross this boundary. See [Client Plugins](/docs/plugins/client) for the complete interface and security model. ## Runtime flow For each application request, Farm runs the runtime hooks in this order: 1. An exact `runtime.endpoints` match runs before ordinary request context and routing. 2. Otherwise, every `runtime.context` creates request-local values. 3. Every `runtime.before` runs in plugin order. 4. Farm calls the page, API route, or integration handler unless a plugin returned a `Response`. 5. Every `runtime.after` can transform the response, including an endpoint response. 6. `runtime.error` observes an error if any preceding phase throws. `runtime.before` may return a new `Request`, return a `Response` to short-circuit, or return nothing. A short-circuit response still passes through `runtime.after`. `runtime.after` may return a new `Response` or return nothing. Runtime hooks use Web APIs, so the same plugin works in development and universal production builds. The event includes `kind`, route metadata, the request `AbortSignal`, and `waitUntil()` for background work supported by the host. An endpoint is intentionally isolated from `runtime.context` and `runtime.before`. Perform any endpoint-specific authentication inside its handler. Global `runtime.after` and `runtime.error` hooks still observe endpoint responses and failures. ## State and context There are three different kinds of plugin data: | Value | Lifetime | Use | | ------- | -------------- | --------------------------------------------------------------------------------------- | | `state` | Plugin manager | Clients, compiled matchers, loggers, or other resources returned by `setup`. | | `ctx` | One request | Typed values returned by `runtime.context`. | | `req` | One request | A shared key/value store for plugins, middleware, integrations, and optional page data. | Farm merges all `runtime.context` results before running `runtime.before`. Context keys must be unique; Farm throws and names both plugins when two plugins return the same key. `ctx` remains server-only. To expose a safe value to a page, write it to `req` explicitly: ```ts runtime: { context({ req }) { const traceId = crypto.randomUUID(); req.set("traceId", traceId, { exposeToPage: true }); req.set("internalToken", "server-only"); return { traceId }; }, }, ``` Only values marked `exposeToPage: true` appear in `props.context?.data`. Farm preserves the request store when a plugin returns a transformed `Request`, buffers a request body, or clones a request for production dispatch. Private values stay private; these replacements do not expose them to page props or another request. ## Ordering Set `enforce` only when hook order is part of the plugin contract: ```ts definePlugin({ name: "auth-context", enforce: "pre" }); definePlugin({ name: "metrics" }); definePlugin({ name: "response-reporting", enforce: "post" }); ``` Farm runs `pre` plugins first, normal plugins second, and `post` plugins last. Hooks that transform a shared value run serially in that order. ## Plugin or integration | Choose | When | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Integration | The extension represents a product or service and needs validated config, endpoints, typed callers, storage, or providers. | | Plugin | The extension changes framework requests, rendering, routing, builds, HMR, or global instrumentation. | | Both | A product integration also needs framework hooks. Keep the product API in the integration and expose its framework behavior through `plugins`. | ## How the ecosystem composes Plugins can be local to one repository, published as packages, bundled by a [Farm layer](/docs/layers), or contributed internally by an integration. They all enter the same ordered lifecycle. ```ts import { defineConfig } from "@farm.js/core"; import { securityPlugin } from "@acme/farm-plugin-security"; import { tracingPlugin } from "@acme/farm-plugin-tracing"; import { routePolicyPlugin } from "./src/plugins/route-policy"; export default defineConfig({ plugins: [ securityPlugin({ csp: true }), tracingPlugin({ service: "storefront" }), routePolicyPlugin, ], }); ``` Each plugin owns its private `state`. At runtime, `ctx` is the read-only aggregate of plugin context results, while `req` is the explicit mutable request store shared with other server lifecycle code. Use stable, namespaced plugin names and context keys so independently published plugins compose without hidden shared globals or collisions. ## Built-in plugins Farm includes plugins for logging, compression, redirects, rewrites, headers, and environment helpers. Register them from `@farm.js/core/plugin/server`. The production compression plugin negotiates Brotli or gzip from `Accept-Encoding`, streams the encoded response body, adds `Vary: Accept-Encoding`, and removes the identity `Content-Length`. Responses that are already encoded, partial, marked `no-transform`, or sent as server-sent events are left unchanged. Dynamic Brotli responses use compression quality 5 to balance response latency and transfer size. Both Brotli and gzip flush each chunk so streaming content can arrive before the response finishes. Gzip keeps its existing compression level; this setting does not change precompressed static assets or compression managed by a deployment platform. Compression applies to media types that actually shrink: every `text/*` type, `application/json`, `application/javascript`, `application/xml`, `application/wasm`, `application/x-ndjson`, any `+json` or `+xml` type such as `image/svg+xml` or `application/manifest+json`, and the uncompressed font formats (`font/ttf`, `font/otf`, `application/vnd.ms-fontobject`). Already compressed payloads are skipped, so images, video, audio, archives, PDFs, Office documents, `font/woff`, `font/woff2` and `application/octet-stream` are streamed through untouched with their identity `Content-Length` and strong `ETag` intact. A response with no `Content-Type` is also left alone, since its bytes may already be compressed. Set a `Content-Type` when a route returns text that should be compressed. ## Legacy hooks Existing flat hooks such as `beforeRequest`, `afterResponse`, `beforeApiHandler`, `afterRender`, `beforeBundle`, and `shutdown` remain supported. They are deprecated where a structured equivalent exists. New plugins should use the grouped interface; do not define both versions of the same phase in one plugin because Farm will run both. The Node-specific `beforeRequest` and `afterResponse` hooks remain available as compatibility escape hatches. Prefer `runtime.before` and `runtime.after` for portable plugins. ## Production checklist - Give every plugin a stable, namespaced `name`. - Keep per-request hooks fast and cancellation-aware. - Return a new `Request` or `Response` instead of mutating Web objects. - Never place credentials or secrets in page-exposed request data. - Use `waitUntil()` only for work that may safely outlive the response. - Make `runtime.error` resilient; an error reporter must not hide the original failure. - Release timers, sockets, and watchers in `runtime.close`. Long-running Node output invokes it for handled shutdown signals; serverless and edge hosts remain platform-specific. - Test server and client hook ordering, short circuits, transformed bodies and headers, context collisions, hydration, SPA navigation, and production output. Continue with [Create a Plugin](/docs/plugins/create-plugin) for complete examples. --- ## Partytown URL: /docs/plugins/partytown Move explicitly opted-in third-party scripts off the browser main thread with a typed Farm.js plugin. # Partytown `@farm.js/partytown` moves compatible analytics and tag-manager scripts into a web worker. Farm serves Partytown's same-origin runtime in development, copies it into the correct production output, and injects its bootstrap. Your application still chooses every script and global call that moves. This is useful when a third-party SDK spends meaningful main-thread time parsing JavaScript or handling analytics events. It is not a blanket switch for every script on a page. ## Install ```bash pnpm add @farm.js/partytown ``` The package includes the Partytown runtime, so no second Partytown dependency is required. ## Full example Create a typed, fire-and-forget caller. This replaces an untyped `window.plausible(...)` access in application components. **src/lib/analytics.ts** ```ts import { defineForward } from "@farm.js/partytown/client"; type PlausibleOptions = { props?: Record<string, string | number | boolean>; }; export const track = defineForward<(event: string, options?: PlausibleOptions) => void>("plausible"); ``` Pass that same value to the plugin. The function carries its forward path, so the configuration and the call site cannot drift onto two different strings. **farm.config.ts** ```ts import { defineConfig } from "@farm.js/core"; import { partytown } from "@farm.js/partytown"; import { track } from "./src/lib/analytics"; export default defineConfig({ plugins: [partytown({ forward: [track] })], }); ``` The vendor URL belongs in the application layout. `type="text/partytown"` is the explicit opt-in that prevents the browser from executing this script on the main thread. **src/app/layout.tsx** ```tsx import type { LayoutProps } from "@farm.js/core"; export default function RootLayout({ children }: LayoutProps) { return ( <> <script type="text/partytown" defer data-domain="yourdomain.com" src="https://plausible.io/js/script.js" /> <main>{children}</main> </> ); } ``` Call the typed bridge from a client component: **src/components/UpgradeButton.tsx** ```tsx "use client"; import { track } from "../lib/analytics"; export function UpgradeButton() { return <button onClick={() => track("Upgrade", { props: { plan: "pro" } })}>Upgrade</button>; } ``` The token or site identifier used by a browser analytics SDK is public configuration. Never place a server secret in the layout, the forward arguments, or any browser bundle. ## SDKs that require initialization Keep provider initialization with the provider script and mark both as Partytown scripts. For example, a Mixpanel-style setup looks like this: ```tsx <script type="text/partytown" src="https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js" /> <script type="text/partytown"> {`mixpanel.init("YOUR_PUBLIC_PROJECT_TOKEN", { track_pageview: true });`} </script> ``` Then define and forward the method used by application code: ```ts export const track = defineForward<(event: string, properties?: Record<string, string | number | boolean>) => void>( "mixpanel.track", ); ``` Use the initialization snippet recommended by the provider when it does more than a direct `init()` call. ## Options ```ts partytown({ forward: [track, "dataLayer.push"], debug: false, fallbackTimeout: 4_000, strictProxyHas: true, }); ``` | Option | Default | Purpose | | ----------------- | --------------------------------- | ---------------------------------------------------------------------- | | `forward` | `[]` | Typed handles or safe dotted global paths to forward into the worker. | | `debug` | Development `true`, build `false` | Select the readable Partytown debug runtime. | | `fallbackTimeout` | Partytown default | Wait time before opted-in scripts fall back to the main thread. | | `strictProxyHas` | Partytown default | Escape hatch for SDKs that depend on strict JavaScript `in` semantics. | Set `fallbackTimeout: 0` when a main-thread fallback is less desirable than dropping the optional third-party script. There is no `enabled` option. Adding `partytown()` enables the plugin; removing it disables the plugin and all of its runtime and build work. ## Preserve an existing function Partytown normally replaces a forwarded main-thread function with its queue. If an existing implementation must continue running, opt in while defining the bridge: ```ts const push = defineForward<(event: Record<string, unknown>) => number>("dataLayer.push", { preserveBehavior: true, }); ``` The bridge still returns `void`. Worker calls cannot synchronously return a vendor result to browser code. ## What Farm handles - The bootstrap and worker files are served under `/~partytown/`, including a configured `basePath`. - Production assets land in Nitro's public directory, including the separate Vercel static output. - Debug worker files are omitted from production unless `debug: true` is explicit. - The bootstrap is an external same-origin script rather than plugin-owned inline JavaScript. - Bootstrap injection is idempotent when an actual script carries `data-farm-partytown`; matching text elsewhere in the document does not suppress it. Partytown's worker scope can coexist with a root PWA service worker because the browser selects the most specific matching scope. ## Compatibility and tradeoffs Partytown proxies browser APIs, but not every third-party script is a good worker candidate. Keep DOM-heavy widgets, consent interfaces, and APIs that require synchronous return values on the main thread. Test each provider's important flows in a production build. The plugin uses Farm's final HTML transform because Partytown's bootstrap must be present in the document head before opted-in scripts run. Farm buffers that document when the plugin is enabled, so this trades streamed HTML delivery for less third-party work on the browser main thread. Measure the result for your application rather than assuming every page wins. For Content Security Policy, allow the vendor's required script and network origins. Farm's own bootstrap and workers stay same-origin. See Partytown's [configuration](https://partytown.qwik.dev/configuration/) and [tested services](https://partytown.qwik.dev/common-services/) when evaluating an SDK. --- ## PWA Plugin URL: /docs/plugins/pwa Generate a route-aware service worker with safe offline navigation, update prompts, and concise SWR image caching. # PWA Plugin `@farm.js/pwa` turns a production Farm build into an installable, offline-aware Progressive Web App. It generates a service worker from the final client output, maps emitted static routes to their HTML files, and registers the worker through Farm's browser lifecycle. Advanced applications can replace the generated worker with a prebuilt JavaScript file while retaining that registration lifecycle. ## Install ```bash pnpm add @farm.js/pwa ``` ## Start with the short configuration ```ts title="farm.config.ts" import { defineConfig } from "@farm.js/core"; import { pwa } from "@farm.js/pwa"; export default defineConfig({ plugins: [ pwa({ offline: "/offline", cache: "auto", }), ], }); ``` The generated worker always precaches immutable build assets. `cache: "auto"` additionally means: - Precache every HTML page Farm emitted as a static route. - Cache public same-origin images with SWR. - Keep up to 100 image responses fresh for 30 days. `auto` is the default, so you can omit `cache` for the same behavior. The previous `recommended` value remains available as a compatibility alias. Service worker updates separately default to `update: "prompt"`. The generated worker only intercepts `GET` requests and same-origin URLs. Dynamic pages, APIs, actions, integrations, and workflows remain network-owned. ## Add the offline page The fallback has to be static because the worker must download it during installation. ```tsx title="src/app/offline/page.tsx" export const ssg = true; export default function OfflinePage() { return ( <main> <h1>You are offline</h1> <p>Reconnect to load fresh server data.</p> </main> ); } ``` The production build fails with a clear message if `offline` does not resolve to emitted static HTML. This prevents a worker from installing with a fallback that can never work. ## Add the application manifest Use Farm's existing metadata route. Farm serves it at `/manifest.webmanifest`, so the plugin does not ask you to repeat application metadata in PWA configuration. ```ts title="src/app/manifest.ts" import type { MetadataRoute } from "@farm.js/core"; export default function manifest(): MetadataRoute.Manifest { return { name: "Acme Field App", short_name: "Acme", start_url: "/", display: "standalone", background_color: "#ffffff", theme_color: "#16a34a", icons: [ { src: "/icon-192.png", sizes: "192x192", type: "image/png" }, { src: "/icon-512.png", sizes: "512x512", type: "image/png" }, ], }; } ``` ## SWR in one line Use the explicit form when you want individual cache controls instead of the automatic preset: ```ts pwa({ cache: { staticRoutes: true, images: "swr", }, }); ``` `swr` means stale while revalidate: 1. The first request downloads and stores the image. 2. A later request returns the cached image immediately. 3. The worker fetches a newer response in the background. 4. The next request receives the refreshed image. Cache Storage is shared by every account that signs in on the same browser profile and survives sign-out, so the worker only stores an image it can safely show to anyone: - The response is marked shareable with `Cache-Control: public` or a positive `max-age`, or has a future `Expires`. - Or it looks like a static file: no `Cache-Control` freshness, but a `Last-Modified` header. Static files in `public/` are served this way. An image is never stored when its request has an `Authorization` header, when its response is marked `private`, `no-store`, `no-cache`, or `max-age=0`, or when it varies on `Cookie`, `Authorization`, or `*`. A response with no caching headers at all is not stored either, which is what an API route returning a per-account image usually sends. To have a dynamic image cached, send an explicit policy such as `Cache-Control: public, max-age=3600`, and only for images that are the same for every visitor. Runtime image caching is best-effort. If browser storage is unavailable or full, a successful network response still loads normally. If the network fails, an available cached image remains the fallback; without one, the network error is preserved. Install-time precaching still requires all selected assets to be cached successfully. Customize the storage bounds with short names: ```ts pwa({ cache: { staticRoutes: ["/", "/help", "/pricing"], images: { strategy: "swr", limit: 200, ttl: "7d", }, }, }); ``` `limit` is the maximum image count. `ttl` accepts milliseconds or `s`, `m`, `h`, `d`, and `w` durations such as `30s`, `5m`, `6h`, `30d`, or `2w`. ## Bring your own service worker Use a custom worker when you need complete control over fetch routing, background sync, push notifications, or a caching strategy outside the generated worker's scope: ```ts title="farm.config.ts" pwa({ serviceWorker: { source: "src/service-worker.js", type: "module", }, update: "prompt", }); ``` `source` is relative to the Farm project root. Farm copies it verbatim to the final `sw.js` path under `basePath`; it does not bundle or transform the file. Module imports must therefore resolve to files available in the production public output. The custom worker owns install, fetch, offline, and cache behavior, so `serviceWorker` cannot be combined with `offline` or `cache`. Farm still registers the worker and exposes its update events. To support `applyUpdate`, handle the message used by Farm's update lifecycle: ```js title="src/service-worker.js" self.addEventListener("activate", (event) => { event.waitUntil(self.clients.claim()); }); self.addEventListener("message", (event) => { if (event.data?.type === "FARM_PWA_SKIP_WAITING") void self.skipWaiting(); }); ``` If you need to control registration and scope as well, leave out the PWA plugin and register the worker directly in application client code. ## Update behavior The default is `update: "prompt"`. When a new service worker finishes installing, the plugin dispatches an event instead of reloading a page that may contain unsaved work: ```ts title="src/client.ts" window.addEventListener("farm:pwa:update-available", (event) => { const { applyUpdate } = (event as CustomEvent).detail; showUpdateButton({ onClick: applyUpdate }); }); ``` Application code can also call: ```ts window.__FARM_PWA__?.applyUpdate(); ``` Set `update: "auto"` only when reloading cannot discard user input. Auto mode activates a waiting worker and reloads once the new worker controls the page. ## Options Adding `pwa()` to `plugins` enables service worker generation and registration. Remove it from the array when the app should not ship a service worker. | Option | Default | Description | | --------------- | ---------- | ------------------------------------------------------------------- | | `offline` | `false` | Static fallback route for the generated worker. | | `update` | `"prompt"` | Prompt or automatically activate and reload for a waiting worker. | | `cache` | `"auto"` | Generated-worker caching, a custom object, or build assets only. | | `serviceWorker` | `false` | Prebuilt worker source and optional `"classic"` or `"module"` type. | | Cache option | Default under `auto` | Description | | -------------- | -------------------- | ---------------------------------------------------- | | `staticRoutes` | `true` | Every emitted static page, a route list, or `false`. | | `images` | `"swr"` | SWR options, `true`, `"swr"`, or `false`. | `offline` and explicit `staticRoutes` entries must be stable root-relative application paths. URLs, query strings, fragments, dot segments, backslashes, control characters, and encoded path separators are rejected. Write these paths without the deployment prefix: with `basePath: "/app"`, `"/"` is served at `/app` and `"/help"` at `/app/help`. An application route named `"/app"` is distinct from the home page and is cached at `/app/app`. Farm handles localized HTML output automatically; include the locale in a localized route such as `offline: "/en/offline"`, but do not add the deployment prefix. ## Production lifecycle During `farm build`, generated-worker mode: 1. Finds the preset's final public output. 2. Hashes precached files and caching options into a deployment-specific cache ID. 3. Writes `sw.js` under Farm's configured `basePath`. 4. Maps clean static route URLs to emitted HTML files. 5. Fails if the configured offline page is missing. Generated cache names are isolated by `basePath`, so multiple Farm applications on one origin do not delete or reuse a sibling application's precache or image cache. Custom-worker mode copies the configured source to the same deployment-aware `sw.js` location and leaves its contents untouched. In the browser, the plugin registers only for production builds. Service workers require HTTPS in production; browsers also permit localhost for development and local production testing. See the complete runnable source in [`examples/pwa-demo`](https://github.com/farming-labs/farm.js/tree/main/examples/pwa-demo). --- ## Plugin Tables URL: /docs/plugins/schema-tables Let a plugin declare the tables its schema needs, so farm <plugin> migrate can create them from the schema alone. # Plugin Tables A plugin that takes a `schema` usually needs somewhere to put the rows. Farm can create those tables from the schema itself, so neither you nor the people using your plugin have to transcribe it into SQL by hand. Declare the tables once and every app gets a command: ```bash pnpm farm <plugin> migrate ``` ## Using it If a plugin you have configured declares tables, the command prints the SQL its schema implies and stops: ```bash pnpm farm sync migrate ``` ```sql -- Generated by `farm sync migrate` from the sync schema. -- Dialect: sqlite -- Review before applying. Farm never alters existing tables. CREATE TABLE IF NOT EXISTS "tasks" ( "id" TEXT PRIMARY KEY, "title" TEXT NOT NULL, "status" TEXT NOT NULL DEFAULT 'open', "listId" TEXT NOT NULL, "updatedAt" TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS "tasks_listId_idx" ON "tasks" ("listId"); ``` Read it, then run it: | Flag | What it does | | ---------------- | ------------------------------------------------------------- | | _(none)_ | print the statements | | `--write <file>` | save them to a file to commit alongside your other migrations | | `--apply` | execute them | `migrate` reports what is _missing from your database_, so it needs to reach it even to print. For a full dump that needs no connection, use `farm generate --orm postgres|mysql|sqlite` — it reads the same declarations and emits the same SQL, plus Prisma and Drizzle schemas. The plugin name is the one the plugin declares, not its package name. Ask an app what it has by naming one that does not exist: ``` $ pnpm farm jobs migrate No plugin named "jobs" owns tables in this app. Available: sync. ``` ### On its own, it only adds When a plugin's tables already exist, `migrate` adds what a table that may hold rows can safely take, and reports the rest: - **Added:** missing columns that are nullable or have a default (existing rows get the default), and missing plain indexes. - **Reported, never applied:** a required column without a default, a unique column or index (existing rows could break it), a changed column, and anything the database has that the schema does not. ``` [info] These tables exist but no longer match the schema. Farm will not change them: tasks missing in the database: slug not in the schema: legacy_note changed column status: default expected "open", found none ``` A create or an addition is derivable from the schema alone. A change is not: a rename and a drop-plus-add look identical from here, and one of them destroys data. That call stays with whoever knows which one it was: the plugin, through a [migration step](#renames-and-other-changes-migration-steps), or you, with your own migration tooling. Statements are written so `--apply` is safe to re-run. ### Upgrading a plugin Farm remembers what it last applied for each plugin, in a `farm_schema_state` table in your database, with the plugin's `version`. When you upgrade the plugin and run `migrate`, it shows what that version changed before touching anything: ``` $ pnpm farm teams migrate teams 1.0.2 → 1.1.0 changes its tables: + invitation new table + member.role string + member_role_idx index Apply these changes to teams's tables now? (y/N) ``` In a terminal it asks; in scripts and CI it prints the plan and stops, and `--apply` applies it. Changes a release makes that Farm will not apply on its own, such as a removed or redefined column, are listed with `~`. `farm schema check` also warns while an upgrade is waiting: ``` ! teams (plugin, postgres) warning teams upgraded 1.0.2 → 1.1.0, and 3 change(s) are not applied yet. ``` The dev server says so too, once it is up: ``` ⚠️ teams 1.0.2 → 1.1.0 has 3 change(s) to its tables that are not applied yet. Run `farm teams migrate` to review them. ``` It only reads, runs in the background after startup, and stays quiet when a database is unreachable, so it never slows `farm dev` down. Apps whose plugins own no tables never run it. Set `version` in `definePlugin` so the summary can name the versions. Without it, Farm says the tables changed since they were last migrated. In a project with Prisma or Drizzle, no `farm_schema_state` table is created: their own migrations track history there, and `farm generate --orm` carries the plugin's new tables and columns into their schema. ### Renames and other changes: migration steps Farm adds on its own, but it never guesses at a rename: from the database, a renamed column looks exactly like one removed and one added, and applying that would leave the data behind. A release that renames ships a step: ```ts title="src/index.ts" definePlugin({ name: "farm:teams", version: "2.0.0", schema: teamsSchema, migrations: [ { id: "2.0.0-role-to-permission", renameColumn: { model: "member", from: "role", to: "permission" }, }, { id: "2.0.0-backfill-owners", description: "mark the first member of each team as owner", sql: { postgres: `UPDATE "member" SET "permission" = 'owner' WHERE ...`, mysql: "UPDATE `member` SET `permission` = 'owner' WHERE ...", sqlite: `UPDATE "member" SET "permission" = 'owner' WHERE ...`, }, }, ], }); ``` | Step | What it does | | -------------- | ------------------------------------------------------------------ | | `renameColumn` | `model` and `to` are your current names; `from` is the old column | | `renameTable` | `to` is the current model; `from` is the old table | | `sql` | your SQL per database; runs after this release's new columns exist | | `dropColumn` | `model` is current; `column` is the one this release removed | | `dropTable` | `table` is the one this release removed | ``` $ pnpm farm teams migrate teams 1.1.0 → 2.0.0 changes its tables: 1. rename member.role → member.permission 2. mark the first member of each team as owner (after the changes below) ``` - Each step runs **once per database**, in order. Farm records it in a `farm_schema_steps` table, and notices a step edited after it ran instead of running it again. - Renames run first, then the release's additions, then custom SQL, so a backfill can fill a column this release adds. Set `before: true` on a `sql` step to run it with the renames. - A **fresh install** runs no steps: tables created at this version already have their effect. A rename someone already did is detected and only recorded. - A step that cannot run safely, such as a rename whose target column already exists, **stops** there: later steps wait, `--apply` exits with an error, and `farm schema check` says why. - Each step is one transaction on Postgres and SQLite. MySQL commits schema changes immediately, so a failed step there can leave part of it applied. - With Prisma or Drizzle, steps are printed for their migrations, never run. **Dropping deletes data,** so `dropColumn` and `dropTable` never run on their own. Without `--allow-destructive` the run stops at the first drop and later steps wait; the terminal prompt cannot skip that: ```bash pnpm farm teams migrate --apply --allow-destructive ``` ``` teams 1.1.0 → 2.0.0 changes its tables: 1. drop member.legacyNote ⚠ deletes data, needs --allow-destructive ``` Farm also only drops what it recorded the plugin creating. A column the app added to a plugin's table, a table another plugin creates, or a table the plugin only describes is never dropped by a plugin's step: the run stops and says so. A step cannot drop something the plugin still declares, and fails when the config loads if it tries. Steps use your plugin's own names, so they follow the app's renames. A plugin that renames without a step gets a hint in the summary: `? if member.role was renamed to member.permission, the plugin needs a migration step`. ### When it does not apply Two setups need nothing, and say so rather than guessing: - **A storage mount.** Key-value storage has no tables. The command reports `Nothing to create` and exits cleanly. - **A schema owned by Prisma, Drizzle, or another ORM.** Their migrations are the source of truth. Keep using them, and wire them into Farm so they run with everything else: ```ts title="farm.config.ts" export default defineConfig({ migrations: { commands: ["pnpm prisma migrate deploy"] }, }); ``` ## Migrating every plugin at once `farm schema migrate` runs every plugin's migration in one go, dependencies first. A plugin whose tables point at another plugin's (`billing` at `teams`) waits for it: ```bash pnpm farm schema migrate # print every plan, in order pnpm farm schema migrate --apply # create the tables pnpm farm schema migrate --write migrations/farm.sql ``` ``` Migrating in order: teams → billing → audit ``` It behaves like running `farm <plugin> migrate` for each plugin in that order, with the same flags, approvals, and Prisma or Drizzle handling. If one plugin fails, the plugins that depend on it are skipped, the rest still run, and the command exits with an error listing what did not finish. Running it again only does what is left. Your own ORM's or library's migrations are not run by it. Run those first, with [`farm migrate`](/docs/cli#run-command-migrations) or their own command. ## Checking everything at once `migrate` looks at one plugin. `farm schema check` looks at every integration and plugin that declares tables, against the database each one uses, and at the tables they point to that Farm does not manage, such as your ORM's `users` or the tables Better Auth creates: ```bash pnpm farm schema check ``` ``` ✗ loyalty (plugin, postgres) error Table "points" does not exist. Run `farm loyalty migrate` to see the SQL, then `--apply` it. error "points.userId" references "users.id", but "users" does not exist. Farm does not manage that table. Run the migration that creates it (your ORM's, or a library's such as Better Auth) first. 2 error(s), 0 warning(s) ``` Errors are things that break at runtime: - a table, or a column, the owner needs is missing, or a column's type changed - a referenced table or column does not exist. When the table belongs to another plugin, the message names it so you know which `migrate` to run first, and it is looked up in that plugin's database - two owners claim the same table in the same database - an owner's database cannot be reached. The other owners are still checked Warnings do not fail the check: - other drift, such as defaults, indexes, and foreign keys - a reference whose column types differ, such as text pointing at a Postgres `uuid`. Lookups by value work, but a SQL join needs a cast and a foreign key is not possible. SQLite compares across types, so it is not flagged there - a client Farm cannot inspect, such as a Prisma client. That ORM's migrations own those tables. Drizzle databases are read through the driver they wrap A referenced table is found the way your app's queries find it: through the connection's search path on Postgres, or as `schema.table` when the reference is dotted, such as `auth.users` in Supabase. Tables a plugin creates are looked up in the current schema, because that is where `migrate` creates them. Connecting, and each query, has 10 seconds to answer, so an unreachable database fails the check instead of hanging it. Pass `--timeout <ms>` to change it. It only reads. Nothing is created or altered, so it is safe to run against production. It exits `1` when there are errors, which makes it a deploy gate: ```yaml - run: pnpm farm schema check ``` Pass `--json` for the report as data. Owners that store data in a key-value mount have no tables and are listed as skipped. ## Declaring tables from a plugin Give `definePlugin` the schema: ```ts title="src/index.ts" import { definePlugin, defineSchema } from "@farm.js/core"; export const teamsSchema = defineSchema({ models: { organization: { fields: { id: { type: "uuid", primaryKey: true }, name: { type: "string" } }, }, member: { fields: { id: { type: "uuid", primaryKey: true }, organizationId: { type: "uuid", reference: { model: "organization", field: "id", onDelete: "cascade" }, }, }, }, }, }); export function teams() { return definePlugin({ name: "farm:teams", schema: teamsSchema }); } ``` That is the whole contract. Apps that configure the plugin get: - `farm teams migrate`: the command is the part of the plugin name after its last `:` or `/` - the plugin's models in `farm generate --orm prisma|drizzle|...`, alongside every integration's - its tables in `farm schema check` The tables live in the app's database, `storage.client` in `farm.config.ts`, the same one integrations use. A broken schema fails when the config loads. ### Tables from another plugin A reference to a table another plugin creates, or columns added to one, makes that plugin a dependency: `farm schema check` lists it first and shows what each plugin needs, as in `billing (plugin, postgres, needs teams)`. Add [`dependsOn`](/docs/plugins/create-plugin#depend-on-another-plugin) as well, so an app that installs your plugin without the other one fails while its config loads instead of at the first query. ### Foreign keys between plugins A reference to another plugin's table becomes a real foreign key when that is safe: the other table is in the same database, the referenced column is its primary key or unique, and the column types match. It is created with the table, so `farm schema migrate` gets it for you by creating the other plugin's tables first: ```sql CREATE TABLE IF NOT EXISTS "subscription" ( "id" TEXT PRIMARY KEY, "organizationId" TEXT NOT NULL REFERENCES "organization" ("id") ON DELETE CASCADE ); ``` If the other plugin has not created its table yet, the table is created without the key and the command says to run `farm schema migrate`. A table that already exists never gets a foreign key on its own: it may hold rows that point at nothing. Allow it, and Farm counts those rows first and adds the key only when there are none: ```ts title="farm.config.ts" schema: { allowForeignKeys: { billing: ["subscription"]; } } ``` SQLite can only add a foreign key when it creates a table. References to the app's own tables, and references with `enforced: "app"` or `"none"`, never get one. ### Tables the plugin uses but does not create A plugin often points at a table the app owns, such as the `user` table its auth library creates. Describe it with `external: true`: ```ts user: { external: true, fields: { id: { type: "string", primaryKey: true } }, }, member: { fields: { // ... userId: { type: "string", reference: { model: "user", field: "id" } }, }, }, ``` Farm never creates an external table: `migrate`, generated Prisma and Drizzle schemas, and conflict checks all skip it, and no foreign key points at it. It is described so the reference resolves to the real table, `farm schema check` can verify that table exists, and the plugin can query it. ### Letting the app use its own names The app's tables may not be called what your schema calls them. Accept renames as an option and apply them with `renameSchema`: ```ts title="src/index.ts" import { definePlugin, renameSchema, type FarmSchemaRenames } from "@farm.js/core"; type TeamsOptions = { schema?: FarmSchemaRenames<typeof teamsSchema> }; export function teams(options: TeamsOptions = {}) { return definePlugin({ name: "farm:teams", schema: renameSchema(teamsSchema, options.schema), }); } ``` ```ts title="farm.config.ts" teams({ schema: { user: { name: "members_auth", fields: { id: "user_id" } }, member: { name: "team_members" }, }, }); ``` Only names change. Migrate creates `team_members`, the check looks for `members_auth.user_id`, and at runtime the plugin keeps its own names: an ORM built from the renamed schema with `createIntegrationOrm` reads `user.id` from `members_auth.user_id`. Renames are typed from the plugin's schema, so the app gets completion, and a model or field that does not exist fails when the config loads. ### Adding columns to the app's tables Sometimes a plugin needs data on a row the app owns, such as points on each user. Describe the table as external and `extend` it with the columns: ```ts title="src/index.ts" export const loyaltySchema = defineSchema({ models: { user: { external: true, fields: { id: { type: "string", primaryKey: true } } }, pointsHistory: { fields: { /* ... */ }, }, }, extend: { user: { fields: { points: { type: "integer", default: 0 } } }, }, }); ``` The app's table may already hold rows, so each column must have a `default` or be `nullable`. Unique, indexed, and primary-key columns cannot be added this way. Both fail when the config loads. Nothing changes in the app's table until the app allows it: ```ts title="farm.config.ts" export default defineConfig({ plugins: [loyalty()], schema: { allowExtend: { loyalty: ["user"] } }, }); ``` List the plugin's model names or your own table names. Without the entry, `farm loyalty migrate` prints the `ALTER TABLE` commented out with the entry to add, `--apply` creates the plugin's own tables but not the columns and exits with an error, and `farm schema check` reports each missing column. With it, `--apply` adds them: ```sql ALTER TABLE "user" ADD COLUMN "points" INTEGER NOT NULL DEFAULT 0; ``` Farm only adds. A column that already exists is never changed, even with a different type: the check warns instead. If the table does not exist yet, the command says so and still creates the plugin's own tables. **When Prisma or Drizzle owns the table**, a column added behind its back is drift its next migration would undo. In a project with `prisma/schema.prisma` or a `drizzle.config.*`, Farm never alters the app's tables. `migrate` and `farm generate --orm` print the line to add to your schema instead: ``` // In the Prisma model mapped to "user": points Int @default(0) ``` ### When the app picks another database Where data lives is the app's decision. By default a plugin's tables go in the app's `storage.client`, and the plugin sets nothing. Some plugins let the app choose a different database for them through their own options, the way Sync takes `sync({ client })`. Forward that choice with `database`: ```ts title="src/index.ts" export function jobs(options: { client?: unknown } = {}) { return definePlugin({ name: "farm:jobs", schema: jobsSchema, // Undefined when the app passed nothing: storage.client is used. database: { client: options.client }, }); } ``` `client` can also be a function that returns the connection. Farm only calls it from tooling (`migrate`, `generate`, `schema check`), never on the request path. It detects what it is given: | Connection | Result | | ----------------------------------------------------------------- | ----------------------------------------------------------- | | `pg`, `mysql2`, `node:sqlite`, or anything with `query`/`prepare` | migrated | | an unstorage mount, or anything with `getItem`/`setItem` | reported as having no tables | | anything else | an error naming your plugin and pointing at its own tooling | Set `database.dialect` when a connection exposes a generic `query` and is not Postgres: the shape alone cannot tell Postgres and MySQL apart. ### Only create what you own Every model in `schema` is a table the plugin creates, except the ones marked `external: true`. When an app hands your plugin a schema it shares with the rest of its code, mark the models the plugin was not given control of as external. A model the plugin was never given is not its table to create. ## What is generated Everything comes from schema metadata, so the SQL is derivable and reviewable before it runs: | Declaration | What it emits | | -------------------------------- | ------------------------------------------------------------------ | | `name` on a model or field | the real table or column name | | `type` | the column type for the target dialect | | `primaryKey: true` | `PRIMARY KEY` | | `unique: true` | `UNIQUE` on the column | | `index: true` | `CREATE INDEX "<table>_<column>_idx"` | | `constraints: [...]` | `CREATE [UNIQUE] INDEX "<table>_<columns>_<type>"` | | `default` | a literal, for strings, numbers, and booleans | | `default: "now"` on a `datetime` | `DEFAULT CURRENT_TIMESTAMP` | | `reference` | Relationship metadata; database enforcement is the default | | `reference.enforced: "db"` | A SQL/Drizzle foreign key or Prisma relation, including `onDelete` | | `reference.enforced: "app"` | Keeps relationship metadata without a database foreign key | | `reference.enforced: "none"` | Keeps the declaration without a database foreign key | Postgres, SQLite, and MySQL are supported. Prisma and Drizzle output preserves database-enforced internal references instead of reducing them to comments. A `reference` to a model outside this owner's schema is left as a comment rather than a foreign key, since the other table may not exist yet. Omitting `enforced` is equivalent to `enforced: "db"`. Application and unenforced references stay available in schema metadata, but SQL/ORM generation and the runtime ORM do not create a database constraint for them. Database-enforced internal references are dependency ordered so parent tables are created before their dependents, and a table can reference itself. Farm rejects cross-table cycles of database-enforced references before emitting SQL because inline foreign keys cannot create either table first; mark one edge `enforced: "app"` or manage that constraint separately when a schema needs that shape. ### Two things worth knowing **Columns are `NOT NULL` by default.** A field is nullable only when it says so: ```ts updatedAt: { type: "datetime" } // NOT NULL updatedAt: { type: "datetime", nullable: true } // nullable updatedAt: { type: "datetime", required: false } // nullable ``` **Names fall back to the schema's own keys**, which is exactly what the runtime reads, so the tables this creates are the tables your queries find. A model `tasks` with a field `listId` becomes `tasks("listId")`, not `tasks("list_id")`. Set `name` on a model or field to point at a different one. **Primary keys follow `primaryKey`, not the field type.** `id` and `uuid` fields without `primaryKey: true` remain ordinary string fields. Farm supports primary keys on `id`, `uuid`, `string`, and `integer` fields; other field types fail during schema resolution before SQL or ORM artifacts are generated. --- ## Scripts URL: /docs/plugins/scripts Load external browser SDKs with typed handles, deliberate timing, consent gates, dependencies, retries, and Farm lifecycle integration. # Scripts `@farm.js/scripts` manages browser SDKs that arrive through external `<script>` tags. It turns an ambient global such as `window.SupportChat` into one typed application handle and owns loading, deduplication, readiness, consent, dependencies, retries, and cleanup of its listeners. Use it for support widgets, analytics, maps, video players, CAPTCHA, payment browser SDKs, experimentation, personalization, advertising, or any other provider that documents a script URL and browser global. Packages installed from npm should still use normal imports. ## Install ```bash pnpm add @farm.js/scripts ``` ## Complete example Define each vendor script in a client-safe module. The local variable can have any name. `global` must match the path the vendor actually creates on `window`. **src/lib/scripts.ts** ```ts import { defineScript } from "@farm.js/scripts/client"; interface SupportChatSDK { load(options: { userId: string; email?: string }): void; open(): void; } export const supportChat = defineScript<SupportChatSDK>({ name: "support-chat", src: "https://cdn.example.com/support-chat.js", global: "SupportChat", load: "manual", preconnect: true, timeout: "10s", retries: 1, }); ``` Register the same handle once in Farm config: **farm.config.ts** ```ts import { defineConfig } from "@farm.js/core"; import { scripts } from "@farm.js/scripts"; import { supportChat } from "./src/lib/scripts"; export default defineConfig({ plugins: [scripts({ scripts: [supportChat] })], }); ``` Use the SDK from a client component without declaring a global `Window` interface: ```tsx "use client"; import { supportChat } from "../lib/scripts"; export function HelpButton({ userId }: { userId: string }) { async function openChat() { await supportChat.use((sdk) => { sdk.load({ userId }); sdk.open(); }); } return <button onClick={openChat}>Talk to support</button>; } ``` The value named `sdk` is the real `window.SupportChat` object after the script has loaded. It is only a callback parameter, so call it `chat`, `client`, `provider`, or anything else that reads well. ## Built-in loading strategies `load` controls the earliest point at which Farm may request the script. | Strategy | Behavior | | --------------------------------------- | ----------------------------------------------------------------- | | `"immediate"` | Starts when Farm's browser plugin runtime starts. | | `"after-hydration"` | Starts after initial hydration. This is the default. | | `"idle"` | Starts in `requestIdleCallback`, with a short fallback timeout. | | `"interaction"` | Starts on the first pointer, keyboard, or touch interaction. | | `"manual"` | Starts only when application code calls `load()` or `use()`. | | `{ when: "visible", selector: "#map" }` | Starts when a matching element approaches or enters the viewport. | Visibility loading observes elements added by client navigation as well as the initial document. Set `rootMargin` to begin before the element reaches the viewport: ```ts const maps = defineScript<MapsSDK>({ name: "maps", src: "https://maps.example.com/sdk.js", global: "ExampleMaps", load: { when: "visible", selector: "[data-map]", rootMargin: "400px" }, }); ``` If `IntersectionObserver` is unavailable, Farm loads once a matching element exists. A manual call always honors dependencies and consent, regardless of the configured automatic strategy. ## Consent Add a consent category to prevent a script request until the application grants it: ```ts export const analytics = defineScript<AnalyticsSDK>({ name: "analytics", src: "https://cdn.example.com/analytics.js", global: "ExampleAnalytics", load: "after-hydration", consent: "analytics", }); ``` Connect your consent interface to the client API: ```ts import { clearScriptConsent, denyScriptConsent, getScriptConsent, grantScriptConsent, } from "@farm.js/scripts/client"; grantScriptConsent("analytics"); denyScriptConsent("advertising"); clearScriptConsent("analytics"); console.log(getScriptConsent("analytics")); // "unknown" | "granted" | "denied" ``` Farm deliberately does not decide categories, display a banner, persist a choice, or claim legal compliance. Your application or consent platform owns those decisions. Grant consent again on each page load after restoring the user's saved choice. Granting a category starts any script whose load trigger already fired. Denying or clearing consent prevents new loads, including scripts waiting for dependencies or a retry delay. A blocked `load()` rejects with `ScriptConsentRequiredError`; granting consent again resumes triggered scripts. Browsers cannot reliably undo requests already started or code that has already executed, so changing consent after insertion does not cancel or unload that script. ## Dependencies Some providers split a base SDK and an extension into separate files. `dependsOn` makes the order explicit without relying on DOM insertion timing: ```ts const maps = defineScript<MapsSDK>({ name: "maps", src: "https://maps.example.com/sdk.js", global: "ExampleMaps", load: "manual", }); const places = defineScript<PlacesSDK>({ name: "places", src: "https://maps.example.com/places.js", global: "ExampleMaps.places", dependsOn: [maps], load: "manual", }); ``` Calling `places.load()` first loads `maps`, waits until it is ready, and then loads `places`. Farm rejects missing, self-referencing, and cyclic dependencies during configuration. ## Handle API Every `defineScript<T>()` call returns the same small interface: | Member | Purpose | | --------------------- | ---------------------------------------------------------------------------- | | `name` | Stable registry name chosen by the application. | | `definition` | Read-only normalized public definition. | | `status` | `idle`, `blocked`, `loading`, `ready`, or `error`. | | `error` | Last terminal loading error. | | `load()` | Loads once and resolves the typed global. Concurrent callers share the work. | | `use(callback)` | Waits for readiness, then calls the callback with the typed global. | | `subscribe(listener)` | Reports status, attempt count, and errors. Returns an unsubscribe function. | For a React status label, subscribe without duplicating loading state: ```tsx "use client"; import { useSyncExternalStore } from "react"; import { supportChat } from "../lib/scripts"; export function ChatStatus() { const status = useSyncExternalStore( (onChange) => supportChat.subscribe(onChange), () => supportChat.status, () => "idle", ); return <span>{status}</span>; } ``` ## Options ```ts defineScript<VendorSDK>({ name: "vendor", src: "https://cdn.example.com/vendor.js", global: "Vendor.sdk", load: "after-hydration", consent: "analytics", dependsOn: [], type: "classic", placement: "head", async: true, timeout: "15s", readyTimeout: "1s", retries: 0, retryDelay: "250ms", preconnect: false, id: "vendor-sdk", integrity: "sha384-...", crossOrigin: "anonymous", referrerPolicy: "strict-origin-when-cross-origin", fetchPriority: "low", attributes: { "data-site-id": "public-site-id" }, }); ``` | Option | Default | Purpose | | ---------------- | ----------------- | ------------------------------------------------------------------------ | | `name` | Required | Unique application registry name. | | `src` | Required | Root-relative path without backslashes, or an absolute HTTP(S) URL. | | `global` | None | Safe dotted browser global returned by `load()` and `use()`. | | `load` | `after-hydration` | One of the built-in loading strategies. | | `consent` | None | App-owned category that must be granted first. | | `dependsOn` | `[]` | Registered handles or names that must load first. | | `type` | `classic` | `classic` or `module`. | | `placement` | `head` | Append to `head` or `body`. | | `async` | `true` | Dynamic script async behavior. Use dependencies for meaningful ordering. | | `timeout` | `15s` | Network load timeout. | | `readyTimeout` | `1s` | Additional wait for the configured global after the load event. | | `retries` | `0` | Extra attempts after a network, timeout, or missing-global failure. | | `retryDelay` | `250ms` | Delay between attempts. | | `preconnect` | `false` | Add one deduplicated cross-origin preconnect hint. | | `id` | None | Script element ID. | | `integrity` | None | Provider-supplied Subresource Integrity value. | | `crossOrigin` | None | `anonymous` or `use-credentials`. `integrity` implies `anonymous`. | | `referrerPolicy` | Browser default | Standard script referrer policy. | | `fetchPriority` | Browser default | `high`, `low`, or `auto` request hint. | | `attributes` | `{}` | Public vendor `data-*` attributes. | Durations accept milliseconds as numbers or short strings such as `250ms`, `5s`, and `1m`. There is no `enabled` option. Adding `scripts()` enables the plugin; removing it disables the behavior. ## Common recipes ### Analytics after consent Use `consent: "analytics"`, load after hydration or on interaction, and expose one app-owned tracking function so provider calls stay in one module: ```ts export async function track(event: string, properties?: Record<string, unknown>) { return analytics.use((client) => client.track(event, properties)); } ``` ### Support chat on demand Use `load: "manual"` and call `use()` from the help button. The SDK adds no network or parse cost for visitors who never ask for help. ### Maps, video, and embeds near the viewport Use the visible strategy with a generous `rootMargin`. This starts loading shortly before the user reaches the placeholder while avoiding work for routes where the element never appears. ### CAPTCHA or payment browser SDK before an action Use `manual`, call `load()` when the form becomes active, and await `use()` during submission. Only public browser keys belong in URLs or attributes. Secret keys stay in server modules. ### Experimentation and personalization Use `immediate` only when the provider must run before hydration and the visual tradeoff is measured. Otherwise prefer `after-hydration`. A third-party script loader cannot guarantee flicker-free experiments by itself. ### Base SDK plus extension Use `dependsOn` instead of assuming two async tags execute in insertion order. Dependency loading is transitive and deduplicated. ### Side-effect-only or module script Omit `global` when readiness means only that the file finished executing. Set `type: "module"` for an external browser module. `load()` then resolves `void`. ### Self-hosted vendor file Place the file in the app's public directory and use a root-relative URL such as `/vendor/chat.js`. Root-relative paths remain stable across routes, and Farm automatically prefixes the configured `basePath` without double-prefixing a URL that already contains it. ### Resilient optional widget Add a short timeout and one retry, subscribe to status, and leave the page usable when the provider is unavailable. Do not make a page's core navigation depend on an optional third party. ## Error handling `load()` and `use()` reject with focused client errors: - `ScriptNotRegisteredError` when the handle was not passed to `scripts()`. - `ScriptEnvironmentError` when browser loading is called during SSR. - `ScriptConsentRequiredError` when its category is not granted. - `ScriptLoadError` for a script element error event. - `ScriptTimeoutError` when the network load exceeds `timeout`. - `ScriptGlobalMissingError` when the file loads without creating `global`. ```ts try { await supportChat.use((chat) => chat.open()); } catch (error) { console.warn("Support is temporarily unavailable", error); } ``` Automatic strategies report terminal failures to the browser console and update the handle status. They do not prevent hydration or navigation. ## Security and limits - Every definition is serialized to the browser. URLs, IDs, attributes, and keys in this config must be public. Never pass a provider secret. - Use a provider-published `integrity` value when the provider offers stable, versioned assets. A mutable latest URL cannot safely use a fixed integrity hash. Integrity needs a CORS request, so Farm sends `crossOrigin: "anonymous"` unless you set `crossOrigin` yourself. - Allow the provider origin in Content Security Policy. `security.csp.nonce` gives dynamic pages a per-response nonce and prerendered pages per-page hashes, but an external `src` still needs its origin allowed. Never place a literal nonce value in Farm config. - Farm validates global paths and only allows `data-*` custom attributes. It does not audit or sandbox vendor code. - A loaded script can modify global browser state. Runtime cleanup removes Farm's observers, listeners, and timers, but cannot reverse vendor side effects. `@farm.js/scripts` runs SDKs on the main thread. Use [`@farm.js/partytown`](/docs/plugins/partytown) for compatible analytics scripts that should execute in a worker. Keep one owner for each script; do not register the same vendor file in both plugins. ## Why no provider presets? The package predefines stable behavior such as loading strategies, consent gates, dependency order, timeouts, retries, and typed readiness. It does not hard-code vendor URLs or initialization snippets, because those contracts change independently of Farm and often differ by account or product tier. Copy the current URL and public options from the provider's official setup guide, then describe its global once with `defineScript<T>()`. See the runnable `examples/scripts-demo` app for typed analytics and support-widget flows using local scripts with no external account. --- ## Search Plugin URL: /docs/plugins/search Generate a self-hosted, serverless search index from Farm's static pages and query it through a typed browser client. # Search Plugin `@farm.js/search` turns Farm's emitted static HTML into a chunked Pagefind index during the production build. The browser downloads only the index data needed for the current query. There is no search server, crawler, account, API key, or application database involved. Use it for documentation, blogs, changelogs, help centers, and prerendered CMS pages. Search for users, orders, messages, or other live application records through a server query, database, or hosted search provider instead. ## Install ```bash pnpm add @farm.js/search ``` ## Create the index Add `search()` to `farm.config.ts`: ```ts import { defineConfig } from "@farm.js/core"; import { search } from "@farm.js/search"; export default defineConfig({ plugins: [search()], }); ``` The default indexes every HTML page emitted by SSG and writes the browser bundle to `/_farm/search/`. Farm automatically prefixes that URL when `basePath` is configured. The build fails with an actionable message when no static page can be indexed. This catches a common mistake where `search()` is added to a fully dynamic application. ## Build a search interface The client is headless and renderer-neutral. React, Preact, Solid, Vue, Svelte, or plain browser code can render the returned data: ```ts import { createSearch } from "@farm.js/search/client"; const siteSearch = createSearch(); const response = await siteSearch.search("server authorization", { limit: 10, }); for (const result of response.results) { console.log(result.title, result.url, result.plainExcerpt); } ``` `createSearch()` does not download the index. The first call to `search()`, `preload()`, or `filters()` loads the Pagefind runtime and the necessary index chunks. Each result includes: | Field | Meaning | | ------------------- | ---------------------------------------------------------------------------- | | `title` | Pagefind title metadata, with a heading or URL fallback. | | `url` | Public route, including Farm's configured `basePath`. | | `plainExcerpt` | Matching text without markup. Safe to render as ordinary text. | | `excerpt` | Matching text with Pagefind `<mark>` elements. Sanitize before rendering it. | | `meta` | Metadata collected from the page. | | `filters` | Filter values assigned to the page. | | `subResults` | Matching headed sections and anchor URLs. | | `score` and `words` | Pagefind relevance score and matching word locations. | Prefer `plainExcerpt` unless the application already has a trusted HTML sanitization path. ## Select routes `include` and `exclude` accept URL patterns. They match application routes before `basePath`, so the same configuration works when an app moves from `/` to `/docs`: ```ts search({ include: ["/", "/docs/**", "/blog/**"], exclude: ["/docs/internal/**", "/blog/drafts/**"], }); ``` `*` matches within one URL segment, `**` matches descendants, and `?` matches one non-slash character. A pattern such as `/docs/**` includes both `/docs` and its descendants. Wildcards can be combined: `/docs/*/**` matches `/docs/en` and `/docs/en/getting-started`, while `exclude: ["/docs/*/internal/**"]` excludes each matching section's internal page and descendants. Route names are preserved even when they repeat the mount path. With `basePath: "/app"`, `include: ["/app/**"]` selects the application's `/app` pages and produces result URLs beneath `/app/app`; it does not select the home page. Custom `output` directories are also relative to `basePath`, so `output: "app/search"` produces `/app/app/search/`. Search result URLs encode emitted filename segments, so a page named `guide#intro` links to `/guide%23intro` instead of a fragment on `/guide`. Spaces, Unicode, and literal percent signs are encoded too. Route filters still use the logical page names before URL encoding and `basePath`. Only emitted HTML can enter the index. Dynamic routes, API routes, server functions, and runtime responses are not crawled. An authenticated page should remain dynamic and must never be made static merely to make it searchable. ## Control page content Pagefind indexes the document body and already ignores elements such as navigation, scripts, forms, and footers. Mark the primary content when a site has repeated chrome: ```tsx export default function GuidePage() { return ( <main data-pagefind-body> <h1>Server functions</h1> <p>Validate input and check authorization before the handler runs.</p> </main> ); } ``` Once any page uses `data-pagefind-body`, Pagefind indexes only pages containing that marker. Add it consistently to every searchable page. Exclude an individual subtree with `data-pagefind-ignore`: ```tsx <aside data-pagefind-ignore>Related navigation</aside> ``` When application markup cannot be changed, configure selectors at the plugin level: ```ts search({ rootSelector: "main", excludeSelectors: [".table-of-contents", "[data-private-note]"], }); ``` ## Metadata, filters, and sorting Pagefind derives a default title from the first heading and supports metadata, filters, and sort keys through HTML attributes: ```tsx <main data-pagefind-body data-pagefind-filter="section:Guides" data-pagefind-sort="date:2026-09-10"> <h1>Server functions</h1> <p data-pagefind-meta="description"> Typed server functions with input validation and authorization. </p> </main> ``` Retrieve available filter counts or apply a filter to a query: ```ts const filters = await siteSearch.filters(); const response = await siteSearch.search("middleware", { filters: { section: "Guides" }, sort: { date: "desc" }, offset: 0, limit: 20, }); ``` Filter expressions also support Pagefind's nested `any`, `all`, `none`, and `not` operators. ## Preload while the user types `preload()` downloads relevant index chunks without executing the search. Call it before a debounced query to make the final response feel immediate: ```ts input.addEventListener("input", (event) => { const term = (event.currentTarget as HTMLInputElement).value; void siteSearch.preload(term); runDebouncedSearch(term); }); ``` Pagefind performs search work in a web worker when supported and falls back to the main thread when workers are unavailable. ## Options ```ts search({ include: ["/docs/**"], exclude: ["/docs/internal/**"], output: "_farm/search", language: "en", includeCharacters: "._$", keepIndexUrl: false, excerptLength: 36, highlightParam: "q", verbose: false, }); ``` | Option | Default | Purpose | | ------------------- | ---------------- | --------------------------------------------------------------- | | `include` | `["/**"]` | Application URL patterns to index. | | `exclude` | `[]` | Application URL patterns to omit. | | `output` | `"_farm/search"` | Bundle directory beneath Farm's public `basePath`. | | `rootSelector` | Pagefind default | Restrict indexed document content to a CSS selector. | | `excludeSelectors` | `[]` | Additional selectors omitted from the index. | | `language` | detected | Force one two-letter ISO 639-1 language for every page. | | `includeCharacters` | Pagefind default | Preserve technical characters inside words. | | `keepIndexUrl` | `false` | Keep `index.html` at the end of result URLs. | | `excerptLength` | `30` | Number of words in result excerpts. | | `highlightParam` | disabled | Query parameter used by Pagefind's optional result highlighter. | | `verbose` | `false` | Print detailed Pagefind indexing diagnostics during builds. | Pass client-only overrides when one page needs a different excerpt or a separately hosted index: ```ts const docsSearch = createSearch({ bundlePath: "/shared/docs-index/", excerptLength: 50, highlightParam: false, }); ``` ## Production and CMS behavior The index is a production artifact. `farm dev` does not crawl the development server because that would produce incomplete results based only on routes somebody happened to visit. Test the real index with a production build: ```bash farm build farm start ``` Publishing new static CMS content must trigger another Farm build so the deployed HTML and index stay in sync. Frequently changing or personalized CMS results should use the provider's search API or a server query instead. The index contains text from public static HTML. Do not place secrets, drafts, tenant data, or authenticated content in SSG output. Route exclusion is organization, not an access-control boundary. ## Runnable example The [search demo](https://github.com/farming-labs/farm.js/tree/main/examples/search-demo) includes a headless React interface, route filters, loading and empty states, plain-text excerpts, and an excluded static page. Pagefind's indexing controls and ranking behavior are documented in the [Pagefind documentation](https://pagefind.app/docs/). --- ## Sentry URL: /docs/plugins/sentry Report Farm server errors with route context, trace requests, and flush pending Sentry events safely. # Sentry `@farm.js/sentry` connects Farm's request, render, build, and shutdown lifecycle to Sentry. It reports server errors with Farm event and route context, names request spans by route pattern, and safely flushes pending events without letting monitoring failures stop the application. ## Install Install the Farm plugin and the Sentry Node SDK: ```bash pnpm add @farm.js/sentry @sentry/node ``` `@farm.js/sentry` provides the Farm lifecycle integration. `@sentry/node` collects and sends the events. The SDK is an optional peer dependency so applications can control its version or supply a compatible SDK explicitly. ## Configure Add the Sentry project DSN to the application environment: ```bash SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 ``` Register the plugin in `farm.config.ts`: ```ts import { defineConfig } from "@farm.js/core"; import { sentryPlugin } from "@farm.js/sentry"; export default defineConfig({ plugins: [ sentryPlugin({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 0.1, }), ], }); ``` The DSN is the only credential needed to report runtime events. A Sentry auth token is only needed for a separate source-map upload workflow. ## Initialize before application modules The Node SDK instruments modules as they load. Initialize it from `src/instrumentation.ts` so HTTP and database instrumentation attaches before the rest of the application starts: ```ts import { registerSentry } from "@farm.js/sentry"; export const register = registerSentry({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 0.1, }); ``` The plugin still captures Farm errors without this file, but automatic HTTP and database instrumentation will not attach. ## What it captures | Farm lifecycle | Sentry behavior | | ------------------------- | ---------------------------------------------------------------------------- | | Runtime start | Resolves and initializes the SDK once. | | Request context | Names the request span by route pattern, such as `GET /users/[id]`. | | Response | Records the response status and finishes plugin-owned spans. | | Runtime and render errors | Captures the exception with the Farm event, request kind, and route context. | | Runtime shutdown | Flushes pending events without allowing a flush failure to stop shutdown. | | Production build | Generates source maps when `sourceMaps` is enabled. | When the Sentry SDK already created an HTTP span, Farm renames that span instead of creating a competing request span. Automatic HTTP and database spans therefore remain correctly nested. ## Serverless deployments Some serverless hosts terminate an invocation without running the normal shutdown lifecycle. Enable response flushing on those deployments: ```ts sentryPlugin({ dsn: process.env.SENTRY_DSN, flushOnResponse: true, }); ``` Farm hands the flush promise to `waitUntil`, allowing supported hosts to keep the invocation alive long enough to deliver pending events. Rejected flushes are logged and do not replace the application response or original error. ## Pass an existing SDK Applications can pass the Sentry SDK module instead of letting the plugin import it: ```ts import * as Sentry from "@sentry/node"; import { sentryPlugin } from "@farm.js/sentry"; sentryPlugin({ sdk: Sentry, dsn: process.env.SENTRY_DSN, }); ``` The `sdk` option accepts the module's structural API, not a separately constructed client. If the application already initialized Sentry, the plugin detects the active client and does not initialize it again. ## Options | Option | Default | Description | | ------------------ | -------------------- | ----------------------------------------------------------------------------- | | `dsn` | | Sentry project DSN. | | `environment` | Instrumentation mode | Environment name. | | `release` | | Release identifier for the deployment. | | `tracesSampleRate` | | Fraction of requests traced. Errors are always reported. | | `sendDefaultPii` | `false` | Include request headers and user data. | | `enabled` | `true` | Set to `false` to keep the hooks registered without reporting. | | `sdk` | `@sentry/node` | Compatible SDK module to use instead of importing `@sentry/node`. | | `flushOnResponse` | `false` | Flush after successful and failed requests for serverless deployments. | | `flushTimeoutMs` | `2000` | Maximum time allowed for each flush. | | `sourceMaps` | `false` | Generate production source maps. This does not upload them. | | `sentryOptions` | `{}` | Extra `Sentry.init` options such as `beforeSend`, `ignoreErrors`, or `debug`. | `sendDefaultPii` remains disabled by default because error events can contain request headers and user information. ## Source maps `sourceMaps: true` generates production source maps but does not upload them to Sentry. Upload the generated maps separately with Sentry's tooling. Enabling source maps moves minification to Nitro's terser, so install its build dependency too: ```bash pnpm add -D @rollup/plugin-terser ``` ## Runtime support The default SDK integration currently supports Node presets. Cloudflare Workers require `@sentry/cloudflare`, so early `registerSentry` initialization is a no-op for `edge` and `bun` runtimes. Browser error reporting also requires a separate browser SDK setup. If a DSN is configured but no SDK resolves, Farm logs a clear error and continues without reporting. Monitoring failures must not prevent the application from starting, responding, or shutting down. --- ## StyleX URL: /docs/plugins/stylex Use StyleX in Farm with extracted production CSS, pre-paint development styles, and HMR. # StyleX `@farm.js/stylex` connects [StyleX](https://stylexjs.com/) to Farm's development and production pipelines. The default setup is one config entry: ```bash pnpm add @farm.js/stylex @stylexjs/stylex ``` ```ts // farm.config.ts import { defineConfig } from "@farm.js/core"; import { stylex } from "@farm.js/stylex"; export default defineConfig({ plugins: [stylex()], }); ``` Farm configures the compiler, puts the development stylesheet in the managed document before the page paints, connects CSS HMR, and extracts rules into the production CSS output. You do not need a Vite plugin entry, virtual-module declarations, a client setup hook, or a manually created StyleX stylesheet. ## Use StyleX Use StyleX's normal authoring API in a component: ```tsx import * as stylex from "@stylexjs/stylex"; const styles = stylex.create({ button: { appearance: "none", backgroundColor: "#111", border: 0, borderRadius: 8, color: "white", cursor: "pointer", paddingBlock: 10, paddingInline: 16, ":hover": { backgroundColor: "#333", }, }, }); export function Button() { return <button {...stylex.props(styles.button)}>Create project</button>; } ``` `stylex.create()` is compiled statically. The browser receives generated class names and extracted CSS rather than a runtime CSS compiler. ## External packages Use `externalPackages` when a dependency publishes source code that still contains `stylex.create()` calls: ```ts export default defineConfig({ plugins: [ stylex({ externalPackages: ["@acme/ui", "shared-stylex-components"], }), ], }); ``` Farm treats each listed dependency like application source: it excludes the package from Vite's dependency optimization and runs the StyleX compiler over its modules. Use package names only: ```ts externalPackages: ["@acme/ui"]; // supported externalPackages: ["@acme/ui/button"]; // package subpath, rejected externalPackages: ["./packages/ui"]; // file path, rejected ``` You do not need `externalPackages` when: - all StyleX calls live in the application; - the dependency publishes already-compiled StyleX output; or - the dependency uses ordinary CSS rather than StyleX. StyleX automatically discovers direct dependencies that declare `@stylexjs/stylex`. Keep `externalPackages` for packages that cannot be discovered through that metadata, workspace packages with unusual manifests, or packages you deliberately need to force through compilation. ## Compiler options The wrapper keeps StyleX's established option names: ```ts stylex({ useCSSLayers: true, importSources: ["@stylexjs/stylex"], externalPackages: ["@acme/ui"], }); ``` | Option | Default | Purpose | | --------------------------- | ---------------------------- | ------------------------------------------------------------------ | | `externalPackages` | `[]` | Force packages containing uncompiled StyleX through compilation. | | `importSources` | StyleX defaults | Recognize compatible StyleX authoring packages. | | `useCSSLayers` | `false` | Place generated rules into CSS layers. | | `babelConfig` | StyleX defaults | Add Babel plugins or presets to StyleX's transform. | | `unstable_moduleResolution` | StyleX CommonJS resolution | Control how StyleX resolves imported constants and themes. | | `lightningcssOptions` | Project Browserslist targets | Configure CSS prefixing and syntax lowering. | | `cssInjectionTarget` | First production stylesheet | Select an existing CSS asset that receives extracted StyleX rules. | Farm intentionally owns StyleX's `dev`, `devMode`, and `devPersistToDisk` options. This keeps the compiler mode aligned with `farm dev` and `farm build` and ensures the development stylesheet and HMR runtime are either enabled together or omitted together. ## Behavior - StyleX registers as a Vite pre-transform and compiles supported JavaScript, TypeScript, JSX, TSX, and Svelte modules reached by Farm's route, server, and client graphs. - Rules found while Farm evaluates server-rendered routes are included in the browser-linked production stylesheet, including styles owned by server components. - Development CSS is served from a same-origin virtual endpoint and is injected into Farm's managed `<head>` before the document reaches the browser. - Production rules are lowered using the project's Browserslist targets and appended to Farm's emitted stylesheet. If the application has no other CSS, StyleX emits a fallback stylesheet. - Enabling the plugin adds compiler work only to modules importing a configured StyleX source. - Configure one `stylex()` instance. Put every external package and compiler option in that one call. Because the development stylesheet must be present in the managed document before paint, the plugin uses Farm's final HTML transform during development. This buffers streamed development HTML; production streaming is unchanged. --- ## Sync Plugin URL: /docs/plugins/sync Turn a schema into browser-side collections with optimistic writes, offline queueing, and warm starts, backed by your own database. # Sync Plugin `@farm.js/sync` gives a model rows that live in the browser. Reads render from a local store, writes apply before the server answers, edits made offline queue until the connection returns, and a revisit paints from disk. It is a layer over your existing data, not a replacement for it: every read and write goes through your database, filtered by a rule you control on the server. > **Choosing between this and the patterns guide** > > [Local-first patterns](/docs/local-first) covers what Farm's cache, mutations, > and persistence already do for ordinary queries and server functions — no new > package. Reach for this plugin when you want the unit of data to be a **row** > rather than a cached response: one row shown in several views, updated once. ## Install ```bash pnpm add @farm.js/sync ``` ## Declare the data once ```ts title="src/schema.ts" import { defineSchema } from "@farm.js/core"; export const schema = defineSchema({ models: { tasks: { fields: { id: { type: "uuid", primaryKey: true }, title: { type: "string", required: true }, status: { type: "enum", values: ["open", "done"], default: "open" }, orgId: { type: "uuid", required: true, index: true }, updatedAt: { type: "datetime" }, }, }, }, }); ``` Field metadata does real work, so configuration does not repeat it: | Declaration | What it decides | | ---------------------------- | ----------------------------------------------------------------- | | `primaryKey: true` | the row key | | `type: "enum"` + `values` | the column's type, and validation before the optimistic write | | `reference` | relationships between models | | `datetime` named `updatedAt` | incremental sync: after the first load only changed rows are sent | | `name` on a model or field | the real table or column name in an existing database | ## Configure the plugin ```ts title="farm.config.ts" import { defineConfig } from "@farm.js/core"; import { sync } from "@farm.js/sync"; import { schema } from "./src/schema"; export default defineConfig({ plugins: [ sync({ schema, storage: "app", models: { tasks: "write", projects: "read" }, middleware: [withOrg], where: ({ context }) => ({ orgId: context.org.id }), }), ], }); ``` ### Options | Option | Purpose | | ---------------- | ------------------------------------------------------------------------------------------------ | | `schema` | the declarative schema above | | `storage` | a mount name from `storage.mounts` | | `client` | your database instead of a mount; see below | | `models` | which models the browser may touch: `"write"`, `"read"`, or `false` | | `where` | the row filter applied to every read and write, server side | | `middleware` | request middleware producing the context `where` reads | | `persist` | keep rows on the device for warm starts. Default `true` | | `path` | the endpoint the browser calls. Default `/_farm/sync` | | `allowedOrigins` | extra origins allowed to call the endpoint, in the `serverActions.allowedOrigins` pattern syntax | ## Point it at your database `storage` names a Farm mount and needs no other setup, which makes it the quickest way to run the plugin. A mount is key-value, so it stores rows as serialized values rather than in tables you can query with SQL. For a real database, pass the connection: ```ts import { Pool } from "pg"; sync({ schema, client: () => new Pool({ connectionString: process.env.DATABASE_URL }), models: { tasks: "write" }, where: ({ context }) => ({ orgId: context.org.id }), }); ``` `@farming-labs/orm` detects which driver the connection needs and builds the data layer from your schema, so the same configuration works for a `pg` pool, a Drizzle or Prisma client, a D1 binding, or a Mongo client. Sync never generates SQL and has no per-database code path. ### Where the tables come from Sync reads and writes rows. It never creates or alters tables, so what you need before the first query depends on where the data lives. | Setup | What you do first | | ------------------------------------------- | ----------------------------------------------------------------- | | `storage: "app"` | nothing. A mount is key-value, so there is no table to create | | A database you already use | nothing. Describe the existing tables with `name` mappings, below | | A raw `pg`, `sqlite`, or `mysql` connection | `farm sync migrate` | | A database behind Prisma or Drizzle | their own migration, as usual | If a query fails with _relation "tasks" does not exist_, the table has not been created yet — sync will not create one mid-request. ### farm sync migrate When sync talks to a database connection directly, it can create the tables your schema describes. The command prints the SQL and stops; `--apply` runs it: ```bash pnpm farm sync migrate ``` ```sql -- Generated by `farm sync migrate` from the sync schema. -- Dialect: postgres -- Review before applying. Farm never alters existing tables. CREATE TABLE IF NOT EXISTS "tasks" ( "id" TEXT PRIMARY KEY, "title" TEXT NOT NULL, "status" TEXT NOT NULL DEFAULT 'open', "orgId" TEXT NOT NULL, "updatedAt" TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS "tasks_orgId_idx" ON "tasks" ("orgId"); ``` Only models you listed in `models` are created. A model declared in the schema but never exposed to the browser belongs to the rest of your app, so sync leaves it alone. Existing tables are reported, never altered, and an ORM-owned schema should keep using its own migrations. See [Plugin Tables](/docs/plugins/schema-tables) for the flags, the drift output, and how any plugin declares tables this way. ### Syncing tables you already have Describe the existing tables instead of creating new ones. `name` maps a model to its table and a field to its column: ```ts tasks: { name: "todo_items", fields: { id: { type: "uuid", primaryKey: true }, title: { type: "string", name: "item_title" }, orgId: { type: "uuid", name: "organization_id", index: true }, updatedAt: { type: "datetime", name: "updated_at" }, }, }, ``` No migration runs. Models you do not describe stay invisible to the browser. ## Read ```tsx title="src/app/tasks/page.tsx" "use client"; import { useLiveQuery } from "@farm.js/sync/react"; export default function TasksPage() { const open = useLiveQuery("tasks", (task) => task.status === "open", { orderBy: (task) => task.updatedAt, direction: "desc", }); if (open.status === "loading") return <Skeleton />; return ( <ul> {open.rows.map((task) => ( <li key={task.id}>{task.title}</li> ))} </ul> ); } ``` The model name is not a loose string. Farm generates a `SyncModels` map into `src/farm.d.ts` from the schema, so `"tasks"` autocompletes, a wrong name is a compile error, and `task` in the predicate is fully typed, enum unions included. There is nothing to import or instantiate for this: write the schema, and the types follow. The predicate runs in the browser over rows already on the device, so any expression is fine. Which rows reach the device is decided separately, by the server's row filter. | Field | Meaning | | ------------------ | ------------------------------------------------------------------------------------------------ | | `rows` | matching rows | | `status` | `"loading"` only on a genuine cold start; a revisit with persistence on is `"ready"` | | `error` | a failed read. Previously loaded rows stay visible | | `pending` | writes in flight against this model | | `queued` | writes waiting for the connection to return (`paused` is the same number) | | `isEmpty` | no rows matched | | `isPersisted(row)` | true once the server confirmed the row; false while it is optimistic | | `failures` | rolled-back writes awaiting the user; see [when a write fails](#what-happens-when-a-write-fails) | `useRow("tasks", id)` subscribes to a single row. ### Deletions and scope changes With an `updatedAt` cursor, a load after the first asks only for rows changed since the last one. A delta cannot describe a row that no longer exists, so the server also sends the key of every row the caller can still see, read with the same row filter as the list. The browser drops any row it holds whose key is missing. That covers rows deleted by another client, rows deleted straight in the database, and rows that left the caller's scope, for example a task moved to another organization. Rows written in the browser are safe from this: an insert the server has not confirmed yet is never dropped, and neither is a row confirmed while the load was in flight. The key list costs one extra key-only query per incremental load. Without an `updatedAt` field every load is a full snapshot, which already replaces the rows on the device. ## Write Reading and writing are separate surfaces: `useLiveQuery` observes, and `useSyncAction` with a model name is the plain write surface. A wrong model name or field on either side is a compile error. ```ts const tasks = useSyncAction("tasks"); tasks.insert({ title: "Write the RFC" }); tasks.update(id, { status: "done" }); tasks.delete(id); ``` The write methods that shipped on the live query result remain as deprecated aliases; new code should keep queries read-only. Each call applies to the local store immediately, so every view showing that row re-renders in the same frame, then persists in the background. Fields the schema fills in — the key, defaults, the cursor, and the columns owned by `where` — can be omitted, and the generated types know it: `insert` marks them optional. Every write returns a handle, usable whichever way suits the call site: ```ts tasks.insert({ title }); // fire and forget await tasks.insert({ title }); // wait for the server await tasks.insert({ title }).persisted; // the same, named explicitly tasks.insert({ title }).catch(showError); // handle the failure inline const handle = tasks.insert({ title }); handle.key; // the row key, available before any network call handle.state; // "pending" | "paused" | "completed" | "failed" ``` Ignoring the handle is safe: a write the interface already rolled back does not surface as an unhandled rejection. ### What happens when a write fails | Situation | Behavior | | -------------------- | ----------------------------------------------------------------------- | | Server rejects it | the row reverts to its previous values, and the handle rejects | | Network error or 5xx | retried with backoff; the row stays on screen throughout | | Offline | the write pauses, `paused` increments, and it sends itself on reconnect | | Reload while pending | the pending write is lost; optimistic state is in memory only | | Deleting a gone row | treated as done, so the row stays removed instead of reappearing | A rejected write reverts on screen, but it does not vanish: it lands in the model's `failures` queue with the exact input that failed, the reason, and two verbs. Render the queue once and every rollback in the app is accounted for: ```tsx { tasks.failures.map((failure) => ( <p key={failure.id} role="alert"> {failure.error.message} <button onClick={() => failure.retry()}>Retry</button> <button onClick={() => failure.dismiss()}>Dismiss</button> </p> )); } ``` `retry()` re-applies the optimistic state and sends the write again; a second refusal records once, not once per attempt. `failures` is a queue rather than a single error slot because fire-and-forget writes can fail out of order, minutes after the gestures that caused them. ## Security `where` is the boundary, and it is enforced by the query rather than by checking what the browser sent: ```ts where: ({ context }) => ({ orgId: context.org.id }); ``` A client calling `db.tasks.update({ id: "another-org-row" })` produces a query filtered by both the id and the scope, so it matches nothing and returns `not_found`. Three consequences follow: - **Columns named by `where` are server-owned.** They are injected on insert and ignored when a client sends them, so no separate read-only list is needed. - **Synced rows are scoped too.** A device only holds rows that passed the filter, so the local store cannot leak another tenant's data offline. - **A writable model with no filter fails the build.** Set `where: false` on a model to expose every row deliberately. The context `where` reads comes from your own middleware, the same kind used by server functions: ```ts const withOrg = createServerMiddleware({ async handler({ request, next }) { const session = await getSession(request); if (!session.user) throw new UnauthorizedError(); return next({ context: { org: await loadOrg(session.user.orgId) } }); }, }); ``` Sync never inspects authentication itself; it reads the object your middleware built, so any auth provider works. Because operations ride the caller's cookies, the endpoint also enforces the same origin contract as server actions: a POST from another site — including a simple request that skips the CORS preflight — is rejected with `403` before the body is read. A trusted second origin, such as a separate marketing site embedding the app, goes in `allowedOrigins`. ## Persistence Rows are stored in the browser so a revisit paints before the network answers. Entries load stale-but-visible and are revalidated on the first read, so disk data is never treated as fresh. Only server-confirmed rows are written; an optimistic guess in flight never reaches disk. Set `persist: false` to disable it, or replace the store: ```ts import { setSyncPersistence, clearSyncedRows } from "@farm.js/sync/client"; setSyncPersistence(myIndexedDbStore); clearSyncedRows(); // whenever the session changes ``` Call `clearSyncedRows()` whenever the session changes without a page reload: sign-out, signing in as someone else, or switching organization. It clears what is on disk and in memory, including the incremental cursor, and drops any load or write still in flight for the previous session. Each model then reloads in full on its next use, scoped to whoever is signed in at that point. ## Server-ruled transitions A plain write cannot express "the server gets to say no before it becomes true". Transitions with rules — complete, claim, approve — are ordinary server functions, written once against your real database, and bound to a model with `useSyncAction` so calling one behaves like every other sync write: ```ts title="src/actions.ts" export const completeTask = createServerFn({ async handler({ input, request }) { const task = await db.tasks.findFirst({ where: { id: input.id, ...scope(request) } }); if (task?.status !== "open") throw new Error("Only an open task can be completed."); return db.tasks.update({ where: { id: input.id }, data: { status: "done" } }); }, }); ``` ```tsx title="src/app/tasks/page.tsx" import { completeTask } from "../actions"; const complete = useSyncAction(completeTask, "tasks", { optimistic: { status: "done" }, }); complete({ id: task.id }); // fire and forget, like any other write ``` The optimistic patch shows the transition instantly; the row the handler returns replaces it when the server confirms, and a refusal rolls it back into the same `failures` queue plain writes use. Every `useLiveQuery` over the model follows along — the action writes the same store the queries read. The patch resolves in tiers, so most actions declare nothing: 1. **Derived.** When the input carries the model's key plus fields that are schema columns — `rename({ id, title })` — those fields are the patch. 2. **Declared.** For transitions the input does not spell out, pass `optimistic` as a patch object, or as `(input) => patch` when it depends on the input. 3. **None.** Skip both and the action still lands: the returned rows commit into the store on arrival. Only the instant preview is missing. The handler returning the changed row (or an array of rows) is what makes the commit precise; return nothing row-shaped and the model refreshes instead. Do not put the outcome in the input to game tier one — the handler owns the transition, the patch is only the client's drawing of it. Anything that is not a row-level change at all stays a plain server function and composes through invalidation: ```ts export const exportBoard = createServerFn({ invalidates: () => [{ key: ["tasks"] }], async handler({ input, context }) { /* ... */ }, }); ``` ## Limits - **Live cross-user updates are not implemented.** Rows refresh on load and after your own writes; another user's change appears on the next load. - **A pending write does not survive a reload.** Durable offline queueing is tracked separately. - **`storage` mounts are key-value.** Point `client` at a database when you need your existing relational tables. - **Tables are never created or altered.** Sync only reads and writes rows; see [where the tables come from](#where-the-tables-come-from). ## Example `examples/local-first` is a runnable task list covering instant writes, warm starts, offline queueing, and scope enforcement. --- ## WebAssembly Plugin URL: /docs/plugins/wasm Import prebuilt WebAssembly modules in browser code and browser workers. # WebAssembly `@farm.js/wasm` connects WebAssembly ESM imports to Farm's development and production builds, including browser workers. Use it for compatible Wasm libraries that process images, parse files, run simulations, or perform other local computation. The plugin handles loading and bundling. Your app supplies the library and its algorithms. Wasm alone does not move expensive work off the main thread; use a worker when the work would block interaction. ## Setup ```bash pnpm add @farm.js/wasm ``` ```ts // farm.config.ts import { defineConfig } from "@farm.js/core"; import { wasm } from "@farm.js/wasm"; export default defineConfig({ plugins: [wasm()], }); ``` Adding `wasm()` is the opt-in. There is no separate `enabled` option. The plugin configures the loader for the browser and each worker build, preserves existing worker plugins, and uses ES-module workers. It supports prebuilt `.wasm` ESM imports, including compatible `wasm-pack --target bundler` output. It does not compile Rust or C/C++. Packages that already initialize their own binary with `fetch`, or use Vite's `?init` / `?url` imports, may not need this plugin. ## Browser targets Version one uses **native top-level await and module workers**, not a compatibility transformer. | Option | Meaning | Default | | -------- | ----------------------------------------------------- | ------------------------------------------------------------------------------ | | `target` | Browser build target, as a string or array of strings | Your `vite.build.target`, otherwise Chrome/Edge 89, Firefox 114, and Safari 15 | ```ts wasm({ target: ["chrome120", "firefox120", "safari17"] }); ``` An explicit plugin target takes precedence over `vite.build.target`. The plugin does not change the server build target. These defaults describe the required module syntax and worker support, not every feature a third-party Wasm library might use. Targeting an older environment such as `es2020` produces a build error when top-level await is needed. Farm validates direct Wasm imports in both browser and worker production builds, including with Vite 8/Rolldown. Explicit `?init` and `?url` imports do not require this top-level-await validation. `worker.format: "iife"` is rejected with an actionable error. Test your actual library in the browsers you support. For example, older Safari versions have a [known limitation with multiple modules importing the same top-level-await module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#browser_compatibility). A compilation target is not a runtime compatibility polyfill. ## Load a module on demand For a module exporting `add`, load it from a client event handler: ```tsx "use client"; import { useState } from "react"; export default function Calculator() { const [result, setResult] = useState("Ready"); async function calculate() { try { const { add } = await import("../wasm/math"); setResult(`Result: ${add(20, 22)}`); } catch (error) { setResult(error instanceof Error ? error.message : String(error)); } } return ( <> <button onClick={calculate}>Calculate</button> <output aria-live="polite">{result}</output> </> ); } ``` The example's local module re-exports a real binary: ```ts // src/wasm/math.ts export { add } from "./math.wasm"; ``` ```ts // src/wasm/math.wasm.d.ts export function add(left: number, right: number): number; ``` Those declarations must describe your binary's actual exports. Farm does not turn arbitrary Wasm exports into unchecked `any` types. For a published library, use its generated types and **import its documented JavaScript entry point** instead of bypassing its initialization or memory-management glue to import an internal binary. Farm can also server-render client components. `"use client"` does not make a top-level import browser-only. Keep browser-dependent libraries inside client event handlers or effects; the plugin does not infer that boundary for you. ## Run expensive work in a worker Register the listener before loading the async Wasm module so an early message is not lost during initialization: ```ts // src/calculate.worker.ts self.onmessage = async ({ data }: MessageEvent<{ left: number; right: number }>) => { try { const { add } = await import("./wasm/math"); self.postMessage({ ok: true, value: add(data.left, data.right) }); } catch (error) { self.postMessage({ ok: false, error: error instanceof Error ? error.message : String(error) }); } }; ``` Create the worker from browser code using Vite's statically discoverable URL syntax: ```ts const worker = new Worker(new URL("./calculate.worker.ts", import.meta.url), { type: "module", }); worker.onmessage = ({ data }) => { worker.terminate(); if (data.ok) console.log(data.value); else console.error(data.error); }; worker.onerror = (event) => { worker.terminate(); console.error(event.message || "Worker could not load"); }; worker.postMessage({ left: 20, right: 22 }); ``` Also terminate the worker when its owner unmounts or cancels. The [runnable example](https://github.com/farming-labs/farm.js/tree/main/examples/wasm-demo) includes cancellation, message-deserialization errors, repeated runs, and an intentional Wasm trap. Farm does not create a worker pool or wrap the standard Worker API. ## Deployment and limits - Production emits Wasm and worker assets through Vite. Farm serves hashed assets at the origin root even when page routes use `basePath`; the example tests a page at `/lab/` loading those assets. Deploy the complete client output, not just JavaScript files. - This plugin's supported scope is browser code and browser workers. It is renderer-neutral; the maintained example uses React. Server, edge, WASI, and Node-native package compatibility require separate testing and are not promised by this plugin. - Loading, compilation, linking, and runtime errors still need application error handling. A missing binary, unsupported Wasm feature, or library-specific requirement is not silently ignored. - A strict Content Security Policy may need `wasm-unsafe-eval` in `script-src` and an appropriate `worker-src`. The plugin never relaxes your security headers automatically. - Threaded libraries using `SharedArrayBuffer` may need cross-origin isolation and additional setup. `wasm()` does not enable those headers or guarantee threaded-library compatibility. The loading implementation is based on [vite-plugin-wasm](https://github.com/Menci/vite-plugin-wasm). See the [working demo](https://github.com/farming-labs/farm.js/tree/main/examples/wasm-demo) for the full executable setup. --- ## WebMCP Plugin URL: /docs/plugins/webmcp Expose a small, explicit, typed browser tool surface to AI agents with the experimental WebMCP API. # WebMCP Plugin `@farm.js/webmcp` connects explicitly registered application tools to the browser's experimental WebMCP API. It uses Farm's client lifecycle to register tools with `document.modelContext`, refresh them across navigation, and unregister them during route cleanup, HMR, or page close. WebMCP lets an agent ask the page to perform a named action, such as searching the visible catalog or adding an item to a cart. It is a browser API, not a network MCP server, so an agent discovers the tools only while the site is open in a supporting browser. WebMCP is currently a [Community Group draft](https://webmachinelearning.github.io/webmcp/), not a W3C Standard. Its API and browser support can still change. ## Install and configure ```bash pnpm add @farm.js/webmcp ``` ```ts title="farm.config.ts" import { defineConfig } from "@farm.js/core"; import { webmcp } from "@farm.js/webmcp"; export default defineConfig({ plugins: [webmcp()], }); ``` The plugin starts the browser adapter. It does not scan routes, server functions, actions, or queries. No application capability is exposed until client code calls `registerWebMCPTool()`. ## Define one tool ```ts title="src/features/products/search-tool.ts" import { defineWebMCPTool } from "@farm.js/webmcp/client"; export const searchProducts = defineWebMCPTool({ name: "search_products", title: "Search products", description: "Search products currently available in this store.", inputSchema: { type: "object", properties: { query: { type: "string", minLength: 1 }, }, required: ["query"], additionalProperties: false, }, annotations: { readOnlyHint: true }, async execute({ query }: { query: string }, { signal }) { const response = await fetch(`/api/products?q=${encodeURIComponent(query)}`, { signal }); if (!response.ok) throw new Error(`Product search failed with ${response.status}`); return response.json(); }, }); ``` The description and JSON Schema tell the agent when and how to call the tool. `execute` runs in the page and receives the browser's cancellation signal. Results must be JSON-safe values. ## Own tools with the route Register a tool only while it is useful. In React, the cleanup returned by `registerWebMCPTool()` fits directly into an effect: ```tsx title="src/features/products/product-tools.tsx" "use client"; import { registerWebMCPTool } from "@farm.js/webmcp/client"; import { useEffect } from "react"; import { searchProducts } from "./search-tool"; export function ProductTools() { useEffect(() => registerWebMCPTool(searchProducts), []); return null; } ``` Mount `<ProductTools />` in the route that owns product search. Farm unregisters the native tool when the component unmounts. If two route transitions briefly register the same name, the newest definition wins; removing it restores the previous live definition. The API itself is renderer-neutral. Vue, Svelte, Solid, and Preact applications can call the same registration function from their own mount and disposal lifecycle. ## Add runtime validation and type inference `inputSchema` is agent-facing metadata. Add `validate` when the application also needs runtime validation before its handler runs: ```ts import { defineWebMCPTool } from "@farm.js/webmcp/client"; import { z } from "zod"; const input = z.object({ productId: z.string().min(1), quantity: z.number().int().min(1).max(10), }); export const addToCart = defineWebMCPTool({ name: "add_to_cart", description: "Add a product and quantity to the signed-in user's cart.", inputSchema: z.toJSONSchema(input), validate: input, async execute({ productId, quantity }, { signal }) { const response = await fetch("/api/cart", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ productId, quantity }), signal, }); if (!response.ok) throw new Error(`Cart update failed with ${response.status}`); return response.json(); }, }); ``` Farm infers the `execute` input from Zod and other Standard Schema validators. It also supports schema objects with `safeParse`, `safeParseAsync`, `parse`, or `parseAsync`. WebMCP sends `inputSchema`, not the validator, to the browser agent. Use your validator's JSON Schema exporter when it has one, as Zod does above. Otherwise provide the JSON Schema explicitly. ## Authorization stays on the server WebMCP input validation is not authentication or authorization. A browser tool is reachable from the browser, just like a form or client-side API call. The API route, action, or query it calls must still check the current session and permissions on every request. ```ts title="src/app/api/cart/route.ts" export async function POST(request: Request) { const session = await requireSession(request); const input = await validateCartInput(request); await assertCanEditCart(session.user.id, input.cartId); return Response.json(await updateCart(input)); } ``` The plugin intentionally keeps tools same-origin and does not expose WebMCP's experimental `exposedTo` option. Be especially careful with secrets, untrusted content in descriptions, and tools that can spend money, publish content, or delete data. ## Annotations Annotations help a browser agent understand the risk of an operation: | Annotation | Use it when | | ---------------------- | ------------------------------------------------------------------- | | `readOnlyHint` | The tool reads data without changing application or external state. | | `untrustedContentHint` | The result can contain user-supplied or third-party content. | | `consequentialHint` | The action is significant or difficult to reverse. | These are hints for the browser and agent. They do not enforce permissions or replace a user confirmation flow in the application. ## Plugin options ```ts webmcp({ unsupported: "warn", debug: true, }); ``` Adding `webmcp()` to `plugins` enables the browser adapter. Remove it from the array to disable it. | Option | Default | Purpose | | ------------- | ----------------------------------- | -------------------------------------------------------------- | | `unsupported` | `"warn"` in dev, `"ignore"` in prod | Warn, ignore, or report an unavailable browser API. | | `debug` | `true` in dev, `false` in prod | Expose `window.__FARM_WEBMCP__` and log tool execution errors. | In debug mode, inspect current metadata with: ```js window.__FARM_WEBMCP__?.getTools(); ``` ## Browser testing Use the current Chrome WebMCP origin trial or local testing flag described in the [Chrome WebMCP documentation](https://developer.chrome.com/docs/ai/webmcp). Unsupported browsers continue running the application normally; only the agent tool surface is inactive. A deployed page also needs the secure context required by the browser API. Localhost is accepted for local testing. The runnable [`examples/webmcp-demo`](https://github.com/farming-labs/farm.js/tree/main/examples/webmcp-demo) registers read and mutation tools, validates inputs with Zod, updates visible UI state, and removes its tools with the route lifecycle. --- ## Instant Preview URL: /docs/preview Expose a running local Farm app through a public URL for sharing, webhook testing, OAuth callbacks, mobile QA, and browser automation. # Instant Preview Expose the Farm app that is already running on your machine through a temporary public URL. ```bash farm dev --port 3000 farm preview --port 3000 ``` `farm preview` is not a deployment command. It does not run `farm build`, upload output, or create a production release. It opens an outbound connection from the CLI to the Farm Preview gateway and forwards public traffic back to your local dev server. Use it when an external system needs to reach the app you are actively developing: - Stripe, Clerk, Better Auth, Supabase, Unkey, or custom webhooks. - OAuth redirect and callback URLs. - Mobile device testing on a real phone. - Sharing a local branch with a teammate. - Browser automation that needs a public HTTPS origin. - Demoing an integration before it is deployed. ## Quick Start Start the app first: ```bash farm dev --port 4324 ``` Open a preview in another terminal: ```bash farm preview --port 4324 --name checkout-test --expires 2h ``` Previews use a Farm.js Infrastructure API key. Without one, the first preview opens [app.farmjs.dev](https://app.farmjs.dev): sign in or create an account (email and password, or GitHub) and confirm the code. The CLI then asks whether to create an API key for this machine; answer yes and it saves the key. If your account already has projects, it offers them, and the one you pick is saved to the app's `.env` as `FARMJS_PROJECT_ID`: ```txt Creating public preview for the running app... Local: http://localhost:4324 Confirm this code in Farm.js Infrastructure: W7KDQ2PL Open: https://app.farmjs.dev/device?user_code=W7KDQ2PL After approving this device, return here. Create an API key for this machine? (Y/n) y Signed in to Farm Preview as your Farm.js Infrastructure account. Use one of your Farm.js Infrastructure projects for this app? 1) Storefront (Acme) Project number, or Enter to skip: 1 Saved FARMJS_PROJECT_ID for Storefront to .env. Relay: wss://preview.farmjs.dev/agent Opening native Farm preview tunnel... Preview URL ready. Public: https://checkout-test.preview.farmjs.dev Expires: Sep 30, 2026, 3:45 PM Forwarding requests through the native tunnel until Ctrl+C. ``` Later previews reuse the saved API key and don't ask again. Pass `--login`, or run `farm login`, to get a new one. In an interactive terminal, omitting `--expires` prompts for a duration and defaults to one hour. In CI and other non-interactive terminals, the hosted default is used. ### Farm.js Infrastructure authentication and dashboard `farm preview` needs one credential, a Farm.js Infrastructure API key: - If `FARMJS_API_KEY` is set, in your shell, your CI secrets, or the app's `.env` files, it is used and there is no sign-in. - Otherwise `farm login`, or the first `farm preview`, signs you in at app.farmjs.dev and asks whether to create an API key for this machine. Yes saves it in a file only your user can read; no creates nothing. It then offers your existing projects; the one you pick goes into the app's `.env` as `FARMJS_PROJECT_ID`. Apps that already set it aren't asked. - `farm logout` forgets that saved key on this machine. The key itself stays valid until you revoke it in Farm.js Infrastructure. 1. With a Farm.js Infrastructure API key in `FARMJS_API_KEY`, the same key analytics uses, the CLI signs in with it and never opens a browser. It reads the key from your shell or your CI secrets, and from the app's `.env` files the way `farm dev` does. Set `FARMJS_API_URL` too if Infrastructure isn't at `https://app.farmjs.dev`: the key is only sent to a gateway that signs in through that Infrastructure. 2. Without a key or saved login, the CLI opens Farm.js Infrastructure, displays a code, and waits for you to confirm the matching code and explicitly authorize your terminal. 3. Return to the CLI. Choose the preview duration when prompted, or pass `--expires`. 4. Once the tunnel is ready, the CLI prints its public URL and a dashboard link. CI never waits for browser approval: set `FARMJS_API_KEY`, otherwise the command fails with instructions. For CI, create a key restricted to the preview's project, with an expiry, rather than an account-wide key. `FARM_PREVIEW_TOKEN` is deprecated: it still works and prints a warning, and `FARMJS_API_KEY` replaces it. Device approval only exists to create the key: the CLI trades the approved sign-in for an API key and keeps nothing else. The gateway verifies that credential or API key whenever it issues a new, name-scoped grant. A grant cannot outlive the verified account session or API key expiry. API keys may be restricted to one project slug: pass the matching `--project` when starting a preview. With the matching Infra access-control gateway, revoking a key also blocks existing preview traffic. **Stop preview** in the detail page revokes that grant across reconnects without stopping your local app; start a new `farm preview` to get another grant. Both transports check access before forwarding. WebSocket relays also check idle connections every five seconds; authority requests have a five-second timeout and fail closed. Work already executed locally cannot be undone. Older gateways without the access hook only enforce key revocation on new grants, so deploy the matching Infra migrations and access endpoint before updating the gateway. Revoking a device account session prevents new grants; stop existing previews separately. The Previews dashboard shows account-owned connections, expiry, and the latest 200 request metadata records per preview. It refreshes every five seconds and marks missing heartbeats as **Stale**, not live. Paths, methods, statuses, durations, and times are recorded; query strings, headers, and bodies are excluded. Paths may still contain app identifiers or embedded credentials; never put secrets in URL paths. This is best-effort activity reporting, not an audit log or your application's stdout/stderr. Your app still runs on your computer or CI runner. ### Projects and preview details One API key can create multiple previews across multiple projects. The key authenticates your account; it does not define a project. Each run has its own preview ID, detail URL, expiry, and request history. Use a different `--name` for simultaneously running previews. ```bash farm preview --project storefront --name checkout-review --expires 2h farm preview --project storefront --name payment-review --expires 1h farm preview --project docs --name docs-review --expires 1h ``` The managed CLI defaults the project to a normalized `package.json` name under `--root` (or the directory name when no package name exists). Set `--project` or `FARM_PREVIEW_PROJECT` to a stable lowercase slug to group runs from different checkouts. Projects are scoped to your account. Older clients are grouped by their preview name. Filter projects on `/dashboard/previews`, then open a preview to inspect its transport, credential ID (never the key itself), request paths, response statuses, and timings. Search requests or filter error responses without collecting payloads. The CLI's dashboard link opens that preview directly. Activity is retained until seven days after preview expiry; it is best-effort metadata, not a complete request archive. This requires matching CLI, gateway, and Infra releases plus database/environment setup. During the relay rollout, the CLI warns and falls back to compatibility gateway polling if the hosted endpoint cannot accept the native WebSocket connection. An explicitly configured `FARM_PREVIEW_RELAY_URL` is tried first as well. If a managed native relay disconnects before the printed expiry, the same CLI process reuses its session-scoped grant and continues through compatibility polling. This preserves the preview until its absolute expiry instead of turning a relay invocation limit into a shortened user session. Open the public URL from another browser, device, webhook provider, or test runner: ```txt https://checkout-test.preview.farmjs.dev ``` Keep both terminals running while you test. Stop the preview with `Ctrl+C`. ## Expiry and Automatic Shutdown Every hosted preview has an absolute expiry enforced by the gateway and relay. Set it explicitly with minutes, hours, or days: ```bash farm preview --expires 30m farm preview --expires 2h farm preview --expires 1d ``` The hosted service may cap the requested duration. The CLI prints the authoritative expiry returned by the service. Heartbeats do not extend it. After expiration, browser visitors see a friendly Farm preview-expired page. The preview lifecycle is tied to the local app. When `farm dev` stops or the configured local target becomes unreachable, the native tunnel closes its hosted relay session and `farm preview` exits. The compatibility path closes its gateway session the same way. The public URL is invalidated as soon as the session closes. Browser navigation receives an expired preview page; API-style requests receive `404`. You do not need to run a separate command to stop or clean up the tunnel. Cancellation also applies to individual requests. If a visitor closes the connection while a slow or streaming local response is still running, the gateway tells the preview agent to abort that localhost request. ## Commands ```bash farm preview farm preview --port 3000 farm preview --host 127.0.0.1 --port 3000 farm preview --url http://localhost:4319 farm preview --name stripe-webhook farm preview --project storefront --name checkout-review farm preview --expires 2h farm preview --login farm preview --dry-run ``` ### Account commands ```bash farm login farm whoami farm logout ``` `farm login` signs in and saves an API key without starting a preview. `farm whoami` prints the gateway, how it signs people in, and which credential `farm preview` would send: `FARMJS_API_KEY` (shown by its first characters, never the whole key), the deprecated `FARM_PREVIEW_TOKEN`, or the saved key. It also says when `FARMJS_API_KEY` is set but not used, and exits with code 1 when `farm preview` would need a browser sign-in, so scripts can check first. `farm logout` deletes the saved key from this machine. It doesn't revoke it; revoke keys in the Farm.js Infrastructure dashboard. All three read the app's `.env` files the way `farm preview` does, and accept `--gateway` and `--root`. When no target is passed, Farm tries to detect a running app from the current project config and common development ports. Pass `--port` or `--url` when the app is running somewhere specific. The pathname in `--url` is a mount point. For example, `--url http://localhost:4319/console` forwards the public preview root to `/console/` and a public `/settings` request to `/console/settings` without allowing the public path to escape that mount. ## Options | Option | Purpose | | ----------------------- | ----------------------------------------------------------------------------------- | | `--port <port>` | Expose a specific local port. | | `--host <host>` | Expose a specific local host. Defaults to `localhost`. | | `--url <url>` | Expose a full local URL. | | `--name <name>` | Request a readable preview URL name. | | `--project <slug>` | Group managed previews under an account-owned dashboard project. | | `--expires <duration>` | Keep the URL live for a duration such as `30m`, `2h`, or `1d`. | | `--login` | Sign in again instead of reusing the saved Farm Preview account. | | `--dry-run` | Validate target detection and print the preview plan without opening a session. | | `--no-probe` | Skip the local reachability check when `--port` is provided. | | `--gateway <url>` | Advanced: use a different Farm Preview gateway. | | `--provider <provider>` | Advanced: use `farm` for the hosted gateway or `local` for a custom local provider. | ## Readable URLs Use `--name` when the URL needs to be pasted into a provider dashboard. ```bash farm preview --port 4324 --name stripe-webhook ``` Farm normalizes the name into a safe subdomain: ```txt https://stripe-webhook.preview.farmjs.dev ``` Names are best treated as temporary handles. Stop and restart the command when you want to rotate the session. ## Request Logging Every remote request is visible in the preview terminal: ```txt GET /api-demo-client -> 200 1139ms GET /@farm/client.js -> 200 1790ms GET /src/app/api-demo-client/page.tsx?import -> 200 1074ms GET /api/hello?name=something -> 200 621ms POST /api/auth/login -> 200 580ms POST /api/users -> 200 1014ms ``` The local `farm dev` terminal also logs the same app-level work: ```txt [FARM] [PAGE] [GET] /api-demo-client - 200 (21ms) [FARM] [API] [GET] /api/hello?name=something - 200 (5ms) [FARM] [API] [POST] /api/auth/login - 200 (5ms) ``` Use the preview terminal to confirm traffic is reaching the tunnel, and use the dev terminal to confirm Farm handled the route, API handler, middleware, or page render. ## Interaction Model The hosted preview forwards HTTP requests from the public URL to your local app. Browser interaction works after the client runtime hydrates. In development, Vite serves many separate modules before hydration completes. Over a public tunnel that can make the first page load feel slower than localhost. Farm queues button clicks that happen before hydration and replays them after React attaches, so an early click on a client component does not silently disappear. After hydration, normal client-side handlers run in the browser and API calls go through the preview URL: ```tsx "use client"; import { apiClient } from "../lib/api"; export default function Demo() { return ( <button onClick={async () => { const result = await apiClient.hello.get({ query: { name: "preview" }, }); console.log(result.data); }} > Fetch </button> ); } ``` That click is visible as `GET /api/hello?name=preview` in both the preview and dev terminals. ## Webhook Testing Run the local app: ```bash farm dev --port 4324 ``` Open a named preview: ```bash farm preview --port 4324 --name stripe-local ``` Use the public route in the provider dashboard: ```txt https://stripe-local.preview.farmjs.dev/api/stripe/webhook ``` When the provider sends an event, the preview terminal shows the public request and the dev terminal shows the Farm API route. This is the fastest way to test integration routes before deploying them. ## OAuth and Auth Callbacks For auth providers that require a public redirect URL, use a named preview URL and paste the callback path: ```txt https://auth-check.preview.farmjs.dev/api/auth/callback ``` Keep the preview running for the whole login test. If you restart with a different name, update the provider callback URL too. ## Hosted Gateway The default gateway is operated by Farming Labs: ```txt https://preview.farmjs.dev ``` Regular Farm apps do not need Vercel, DNS, Redis, ngrok, `cloudflared`, or a separately installed tunnel binary. The CLI loads `@farm.js/tunnel`, tries to open one outbound WebSocket to the gateway, and forwards requests through its native Rust transport. Compatibility gateway polling keeps previews available while the hosted relay is being rolled out. The hosted gateway owns: - TLS for `*.preview.farmjs.dev`. - Developer identity, name authorization, and absolute session expiry. - Short-lived, single-preview tunnel grants. A shared relay secret is never sent to developers. - Request and response relay over the persistent WebSocket or compatibility polling path. - Stale session cleanup. The local CLI owns: - Local target detection. - Farm.js Infrastructure sign-in and the saved API key. - Native tunnel lifecycle and local reachability checks. - Forwarding requests to `localhost`. - Request and response logging. - Closing the preview when the local app exits. Preview transports currently buffer bodies while carrying them through the relay. Public request and local response bodies are limited to 5 MiB by default; an oversized request receives `413`, while an oversized local response receives `502`. Self-hosted gateway and relay operators can configure those limits. ## Self-Hosting a Gateway Most app developers should use the hosted gateway. Self-host only when you are operating a private preview domain or maintaining Farming Labs infrastructure. The gateway example lives in `examples/preview-gateway`. ```bash cd examples/preview-gateway pnpm install vercel deploy ``` The Vercel example can use Vercel Blob as the session store. Configure a private Blob store for the Vercel project, then set the public domain: ```bash vercel blob create-store farm-preview-gateway --access private --region iad1 --yes vercel env add FARM_PREVIEW_DOMAIN production ``` For managed login, generate two secrets and keep them only on the server: ```bash openssl rand -hex 32 vercel env add FARM_PREVIEW_AUTH_SECRET production vercel env add FARM_PREVIEW_GATEWAY_SECRET production ``` `FARM_PREVIEW_AUTH_SECRET` signs the short-lived tunnel grants. `FARM_PREVIEW_GATEWAY_SECRET` lets the gateway check API keys with Farm.js Infrastructure at app.farmjs.dev; set the same value there. Neither ever reaches a developer machine. `FARM_PREVIEW_DEFAULT_TTL_MS` and `FARM_PREVIEW_MAX_TTL_MS` optionally control the default and maximum hosted durations. See the gateway example README for the deployment order and activity cleanup contract. For a private domain such as `preview.example.com`, configure: - `preview.example.com` for the gateway root. - `*.preview.example.com` as a wildcard domain. - `FARM_PREVIEW_DOMAIN=preview.example.com`. Then point the CLI at it: ```bash farm preview --gateway https://preview.example.com --name checkout-test ``` ## Custom Tunnel Providers If you need to integrate a private tunnel provider, set `FARM_PREVIEW_TUNNEL_COMMAND` to a command template that prints the public HTTPS URL after opening the tunnel. ```bash FARM_PREVIEW_TUNNEL_COMMAND='farm-preview-agent tunnel --url {url} --hostname {hostname}' farm preview ``` Available template values are `{url}`, `{port}`, `{host}`, `{name}`, and `{hostname}`. Use this path only when the hosted Farm gateway is not appropriate for your environment. ## Environment Variables | Variable | Purpose | | ------------------------------- | --------------------------------------------------------------------------------------------------------------- | | `FARM_PREVIEW_GATEWAY_URL` | Override the hosted gateway URL. | | `FARM_PREVIEW_RELAY_URL` | Override the persistent WebSocket relay URL. | | `FARM_PREVIEW_DOMAIN` | Override the preview domain used for generated hostnames. | | `FARM_PREVIEW_NAME` | Provide a default readable preview name. | | `FARM_PREVIEW_PROJECT` | Stable dashboard project slug; overridden by `--project`. | | `FARM_PREVIEW_PROVIDER` | Select `farm` or `local`. | | `FARM_PREVIEW_TUNNEL_COMMAND` | Command template for a custom local tunnel provider. | | `FARMJS_API_KEY` | Farm.js Infrastructure API key. Signs previews in without a browser on a gateway that uses that Infrastructure. | | `FARMJS_API_URL` | Farm.js Infrastructure's address, if it isn't `https://app.farmjs.dev`. | | `FARM_PREVIEW_TOKEN` | Deprecated. Use `FARMJS_API_KEY`. Still accepted, with a warning. | | `FARM_PREVIEW_CREDENTIALS_PATH` | Override the local managed-preview credential file. | ## Troubleshooting | Symptom | Check | | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `No active Farm preview is running` | The CLI session stopped, expired, or the local app exited. Restart `farm preview`. | | `The local Farm preview did not respond before the gateway timed out` | Confirm `farm dev` is still running and the local route is not hanging. | | Public page loads, but clicks feel delayed | Wait for the first hydration pass. Dev-mode Vite modules are forwarded through the tunnel. Early button clicks are queued and replayed. | | No traffic appears in the preview terminal | The browser may still be loading modules, the URL may be stale, or the request may be blocked before reaching the gateway. | | Traffic appears in preview terminal but not `farm dev` | The local target may be wrong. Re-run with `--port` or `--url`. | | HMR websocket errors appear in the browser console | The hosted preview currently forwards HTTP traffic. Vite HMR websocket forwarding is not part of the hosted gateway yet. | | Webhook provider receives a non-2xx response | Check the local dev terminal for the actual Farm API route error. | ## Security The preview URL forwards public internet traffic to your local app. Treat it like a temporary public deployment: - Stop the command when testing is finished. - The CLI saves only the API key, in a file created with user-only permissions. Use `--login` or `farm login` to replace it, and `farm logout` to remove it. - Tunnel grants are bound to one normalized preview name and one absolute expiry. Current polling control requests send session credentials in the `Authorization` header. The native agent is configured with one relay URL, so its name-bound grant is carried only on the outbound `/agent` WebSocket URL; it is never added to the public preview URL. Pre-auth compatibility gateways may still require their random session token in control-request query strings. Gateway operators should redact `/agent` and legacy control-request query strings from access logs. - Do not expose admin-only routes, local dashboards, or secret-bearing pages unless you trust the audience. - Do not paste a preview URL into untrusted systems. - Rotate preview names after sharing sensitive routes. - Keep provider secrets in environment variables, not client code. - Remember that anyone with the URL can send requests while the session is active. ## Production Deployments Use `farm preview` for temporary access to a running local app. Use `farm build` and the deployment guide for production releases. ```bash farm build ``` Preview is for the branch you are holding in your editor. Deployment is for the version you want to keep online. --- ## Project Structure URL: /docs/project-structure The compact file layout Farm expects, plus the optional files you add only when the app needs them. # Project Structure The compact file layout Farm expects, plus the optional files you add only when the app needs them. ## Minimal shape A Farm app can be as small as src, farm.config.ts, package.json, and tsconfig.json. The framework discovers pages, API routes, middleware, layouts, docs, markdown mirrors, and generated route types from there. **Minimal app** ```txt my-app/ src/ app/ layout.tsx # Preact/Solid also use TSX; Vue/Svelte use their component extensions page.tsx farm.config.ts package.json tsconfig.json ``` ## Common folders | Path | Purpose | | -------------- | ---------------------------------------------------------------------------- | | src/app | Pages, nested layouts, API routes, route boundaries, middleware. | | src/client.ts | Optional typed browser lifecycle for HTML-first application enhancements. | | src/lib | Shared server and client utilities. | | src/components | Reusable UI and client components. | | layers | Optional local Farm layers consumed through `extends`. | | src/farm.d.ts | Generated project types for routes, env, and i18n. | | farm.config.ts | Framework config, integrations, docs, KV storage, databases, and deployment. | ## Optional files stay optional Use vite.config.ts only when you need custom Vite behavior. Use platform files only when a deployment target requires provider-specific settings that Farm cannot infer. ## Route files | File | Used for | | ------------------------------------------------------------ | ------------------------------------------ | | `page.tsx` / `page.jsx` / `page.vue` / `page.svelte` | The route UI for the selected renderer. | | `page.md` / `page.mdx` | React-oriented Markdown-first app pages. | | `layout.tsx` / `layout.jsx` / `layout.vue` / `layout.svelte` | Shared shell for every child segment. | | `loading.*` | Pending UI for async route work. | | `error.*` | Segment-level error UI. | | `not-found.*` | Segment-level 404 UI. | | `middleware.ts` | Request behavior before the route renders. | | `route.ts` | API handlers for the current URL segment. | Use `.tsx` or `.jsx` with React, Preact, and Solid, `.vue` with Vue, and `.svelte` with Svelte. See [Renderers](/docs/renderers) for renderer-specific conventions and current feature boundaries. ## Recommended app layout **Project** ```txt my-app/ src/ app/ api/ hello/ route.ts dashboard/ layout.tsx page.tsx about/ page.mdx layout.tsx page.tsx components/ account-menu.tsx lib/ api.ts integrations.ts farm.config.ts package.json tsconfig.json ``` Keep framework configuration in `farm.config.ts`. Keep application helpers under `src/lib`, and keep UI that is reused across pages under `src/components`. Reusable product areas can live under `layers/<name>` with their own optional `farm.config.ts` and `src` directory. The application enables them explicitly through `extends`; see [Layers](/docs/layers). ## HTML-first client lifecycle Add `src/client.ts` when server-rendered HTML needs small browser enhancements without turning a page or layout into a hydrated component tree. Farm discovers the file at build time, includes it in the existing client runtime, and invokes it on the initial document and fragment navigations. ```ts import { defineClient } from "@farm.js/core/client/lifecycle"; export default defineClient({ setup({ router }) { const controller = new AbortController(); document.addEventListener( "click", (event) => { if (event.target instanceof Element && event.target.closest(".js-refresh")) { void router.refresh?.(); } }, { signal: controller.signal }, ); return controller; }, close({ state }) { state.abort(); }, }); ``` The entry is ordinary typed TypeScript, not an inline script string. It does not hydrate the selected renderer unless the application imports and mounts that renderer itself. Serializable values in `publicRuntimeConfig` are provided as the typed `public` field of `setup`, so environment-derived public endpoints can stay in `farm.config.ts`. ## Generated files Farm keeps project-specific route, environment, and internationalization declarations in one generated `src/farm.d.ts` file. Static image and stylesheet declarations come from `@farm.js/core`, so global CSS side-effect imports and CSS Modules type-check without an application-owned declaration file. Generated API clients remain in `src/lib/api.generated.ts` because applications import that module directly. After upgrading an older project, run `farm generate` once to record the current framework type references. Existing generated files that import only `@farm.js/core/image` remain compatible with the stylesheet declarations. `farm dev` keeps generated types current as source files change; run the command manually when the dev server is not running. **Terminal** ```bash farm generate ``` Generated route types narrow route component props and make the React `Link` API stricter. Generated API types make `api.hello.post(...)` match the route's body and query schemas in every renderer. ## When to add root files | File | Add it when | | -------------------- | ----------------------------------------------------------------------------------------------------------- | | `vite.config.ts` | You need Vite plugins, aliases, or server settings Farm does not infer. | | `vercel.json` | You need platform behavior outside Farm's deploy output. | | `tailwind.config.ts` | Your Tailwind version or design system requires an explicit config. | | `docs.config.ts` | A large docs configuration is easier to maintain outside the canonical `docs` property in `farm.config.ts`. | --- ## Query and Params URL: /docs/query Parse search params and route params with typed helpers on the server and synchronized state on the client. # Query and Params Parse search params and route params with typed helpers on the server and synchronized state on the client. ## Server parsing Page `search` / `searchParams` values are strings for single keys and ordered arrays for repeated keys, including empty values. Names such as `toString` and `hasOwnProperty` are ordinary query data, not inherited JavaScript methods. Farm omits `__proto__`, `constructor`, and `prototype` when converting request search parameters to objects. This representation is shared by development rendering, production SSR, page-data navigation, and hydration. **src/app/search/page.tsx** ```tsx import type { PagePropsSafe } from "@farm.js/core/query"; import { asInteger, asString, loadSearchParams } from "@farm.js/core/query/server"; export default async function SearchPage({ searchParams }: PagePropsSafe) { const query = await loadSearchParams(searchParams, { q: asString.withDefault!(""), page: asInteger.withDefault!(1), }); return <pre>{JSON.stringify(query, null, 2)}</pre>; } ``` ## Client query state **src/components/search-controls.tsx** ```tsx "use client"; import { asString, useQueryState } from "@farm.js/core/query/client"; export function SearchControls() { const [q, setQ] = useQueryState("q", asString.withDefault!(""), { throttleMs: 150, }); return <input value={q} onChange={(event) => setQ(event.target.value)} />; } ``` `throttleMs` coalesces rapid writes to the same query key. Updates to different keys are composed against the latest URL, and returning a value to the current URL cancels its queued write. The latest edit to each key wins across hook instances and overlapping multi-key updates, even with different throttle durations or an immediate writer. Superseding one key keeps unrelated keys in an older batch queued. Cancelling the newer edit does not restore an older superseded value. Hook values update immediately while the URL write is queued. Inline parsers, inline parser maps, parent rerenders, and other consumers of the same keys preserve those pending values; memoizing parsers is not required. Each queued key keeps its draft when another key commits. Consumers sharing a key parse incoming edits with their own parser, including while a URL write is queued. Different parser types therefore retain their own output types and defaults; removing a key is parsed like an absent URL value. Multiple `useQueryState` writers receive each other's edits immediately, even when both write in the same event before a throttled URL commit. A writer ignores only its own draft notification; there is no time window during which peer edits are dropped. If a component changes the key or parser it passes to the hook, the returned value is immediately re-parsed from the current URL plus active queued values for that URL. A queued write is cancelled when its owning hook changes keys or unmounts. Navigation to a different URL also discards its pending edits, including in a persistent layout; returning to the old URL does not restore discarded drafts. Writes to different query keys still compose across Farm's own query updates. Repeated keys have the same meaning during server rendering and in client hooks. For example, `?tag=react&tag=vite` is read as both values by `asArrayOf(asString)`. When Farm's SPA router is installed, shallow query pushes participate in its normal history index. Back/forward blockers therefore receive the rendered query location and can restore a blocked traversal without inserting a duplicate entry. ## Multiple query values Use `useQueryStates` when several controls should update together. This keeps the browser URL as the source of shareable state for filters, pagination, and tabs. Changing the parser map replaces the returned object with exactly the newly declared keys. `useQueryStates` receives drafts from both `useQueryState` and other `useQueryStates` instances immediately, before a throttled URL write. Each changed key is parsed with the receiving map's parser; unrelated local drafts are preserved, and the writer does not parse its own draft echo. Changing the parser map replaces its keyed subscriptions, and unmounting removes them. Receiving a peer's draft does not take ownership of its URL timer, so unmounting a reader does not cancel the writer's queued update. **src/components/product-filters.tsx** ```tsx "use client"; import { asInteger, asString, useQueryStates } from "@farm.js/core/query/client"; export function ProductFilters() { const [filters, setFilters] = useQueryStates( { q: asString.withDefault!(""), page: asInteger.withDefault!(1), plan: asString, }, { history: "replaceState", shallow: true, }, ); return ( <form> <input value={filters.q} onChange={(event) => { setFilters({ q: event.target.value, page: 1, }); }} /> </form> ); } ``` ## Route params Use route param parsers when dynamic segments should be typed before they hit your data layer. **src/app/users/[id]/page.tsx** ```tsx import type { PageProps } from "@farm.js/core"; import { asString, loadRouteParams } from "@farm.js/core/query/server"; export default async function UserPage({ params }: PageProps) { const { id } = await loadRouteParams(params, { id: asString, }); return <main>User {id}</main>; } ``` When `parseRouteParams` or `loadRouteParams` receives `URLSearchParams`, repeated values retain URL order. The default `asArrayOf` format and scalar parsers receive the comma-joined value. A structured `asArrayOf` parser keeps every repeated entry as a distinct item, including commas within an item. Object-based route params are unchanged. ## Pagination metadata `createPaginationMeta(searchParams, { totalItems, itemsPerPage })` returns a safe page, offset, limit, and next/previous flags for server-rendered lists. Missing, malformed, zero, negative, or unsafe `page` values fall back to page 1. `totalItems` must be a non-negative safe integer and `itemsPerPage` must be a positive safe integer; invalid configuration throws a `RangeError`. ## Parser reference | Parser | Reads | | ------------------- | --------------------------------------------------------- | | `asString` | Plain strings. | | `asInteger` | Complete, safe integer values for pagination and limits. | | `asFloat` | Complete finite decimal or exponent values. | | `asBoolean` | Boolean flags. | | `asArrayOf(parser)` | Repeated values serialized through another parser. | | `asJson` | Structured JSON encoded in the URL. | | `asIsoDate` | Calendar-valid `YYYY-MM-DD` values. | | `asIsoDateTime` | Calendar-valid ISO date-times with `Z` or numeric offset. | `asArrayOf` keeps its existing comma-separated format by default. When items can contain commas, opt in to the structured format so generated URLs round-trip those values: ```ts const locations = asArrayOf(asString, { format: "structured" }); ``` The item parser still decides how each value is normalized. `asString` trims surrounding whitespace and treats an empty string as missing. Use an exact string parser when those values are significant: ```ts import { asArrayOf, createParser } from "@farm.js/core/query"; const exactString = createParser<string>({ parse: (value) => value, serialize: (value) => value, }); const labels = asArrayOf(exactString, { format: "structured" }); ``` The structured parser still accepts ordinary comma URLs during migration and emits a versioned `~farm-array:v1:` representation only when the comma format would lose information. Because the default parser never reserves that namespace, existing literal values such as `~farm-array:v1:["legacy"]` retain their comma-format meaning. ## Production notes - Parse query on the server before passing values to database queries. - Use defaults for values that should always be present in UI state. - Use `replaceState` for filters that change often, and `pushState` when each change should be browser-history navigable. - Keep large state out of the URL; store only the values users should be able to share. --- ## Reference URL: /docs/reference A compact map of the main package exports and where to learn more. # Reference A compact map of the main package exports and where to learn more. ## Core exports | Export area | What it covers | | ----------------------------- | ------------------------------------------------------------------------------------ | | @farm.js/core | Config, app types, plugins, integrations, routing, OpenAPI, docs, cache. | | @farm.js/core/client | Link, router helpers, callable route actions, API client, integration client. | | @farm.js/core/api/client | Renderer-neutral typed API clients. | | @farm.js/core/plugin/client | Advanced browser lifecycle manager and client plugin event types. | | @farm.js/core/navigation | Next-compatible redirect, notFound, and client navigation hooks. | | @farm.js/core/headers | Next-compatible request headers and cookies helpers. | | @farm.js/core/json-ld | Typed schema.org JSON-LD builders for `metadata.jsonLd`. | | @farm.js/core/router | Lightweight route matching, href building, and active-route checks. | | @farm.js/core/query | Query and route param types. | | @farm.js/core/storage | Key/value clients, drivers, mounts, and `getStorage()`. | | @farm.js/core/cache | Data cache, revalidation, cache keys. | | @farm.js/cache-redis | Distributed Redis cache, tag versions, and regeneration leases. | | @farm.js/core/after | Post-response server work with `after()`. | | @farm.js/core/og | Server-only `ImageResponse` for PNG images rendered from JSX (install `@vercel/og`). | | @farm.js/core/cron | Cron route authorization, schedule types, manifests, and deployment adapter helpers. | | @farm.js/core/observability | Farm events, request tracing, custom spans, and active trace context. | | @farm.js/core/instrumentation | Startup and shutdown convention types for development and production runtimes. | | @farm.js/otel | Node OpenTelemetry SDK, OTLP exporter, auto-instrumentation, and Farm event metrics. | | @farm.js/react | React renderer descriptor and experimental AOT local-state compiler. | | @farm.js/preact | Preact renderer descriptor, streaming SSR, hydration, and Vite integration. | | @farm.js/solid | Solid renderer descriptor, SSR runtime, browser hydration, and Vite integration. | | @farm.js/vue | Vue renderer descriptor, SFC compilation, SSR runtime, and browser hydration. | | @farm.js/svelte | Svelte renderer descriptor, component compilation, SSR, and browser hydration. | | @farm.js/integrations | Auth, billing, email, jobs, AI, API keys, provider clients. | ## Recommended reading path 1. Start with Getting Started and Project Structure. 2. Choose a renderer, then read Routing, Layouts, and Rendering Model. 3. Add API Routes, API Client, and Query. 4. Choose integrations, KV storage, and database ownership once your product needs them. 5. Finish with Deployment, Observability, and Reference. ## Integration exports | Package | Exports | | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `@farm.js/core` | `defineIntegration`, `integrationRoute`, `defineSchema`, `definePlugin`, `defineConfig`. | | `@farm.js/core/theme/client` | `useTheme`, `getTheme`, `setTheme`, and `toggleTheme` for browser color-mode state. | | `@farm.js/core/theme/server` | `getTheme` and `getThemeSnapshot` for cookie-backed server rendering. | | `@farm.js/core/cron` | `cronRoute`, cron config types, schedule manifests, and deployment adapter helpers. | | `@farm.js/core/workflows` | Legacy workflow-module APIs kept for compatibility. New schedules should use `cron` config and API routes. | | `@farm.js/core/client` | `useAction`, `createIntegrations`, `createIntegrationClient`, `createIntegrationServerClient`, `endpoint`. | | `@farm.js/core/plugin/client` | `createClientPluginManager` and browser lifecycle types for advanced tooling. | | `@farm.js/core/navigation` | `redirect`, `permanentRedirect`, `notFound`, `useRouter`, `usePathname`, `useSearchParams`. | | `@farm.js/core/font` | Build-time `localFont` and `remoteFont` loaders with generated CSS, hashed assets, and preload hints. | | `@farm.js/core/headers` | `headers`, `cookies`. | | `@farm.js/core/json-ld` | `article`, `breadcrumbList`, `faqPage`, `product`, and `softwareApplication` JSON-LD builders. | | `@farm.js/core/router` | `createFarmRouter`, `matchFarmRoute`, `buildFarmRoutePath`, `isFarmRouteActive`. | | `@farm.js/core/storage` | `sqliteStorage`, `postgresStorage`, `redisStorage`, `createStorageClient`, `defineStorageClient`. | | `@farm.js/cache-redis` | `redisCache` distributed cache adapter. | | `@farm.js/federation` | `federation` build plugin for browser Module Federation producers and hosts. | | `@farm.js/federation/client` | Typed `loadRemote` and `preloadRemote` browser helpers. | | `@farm.js/health` | `health` plugin for dependency-aware liveness, readiness, and startup probes. | | `@farm.js/search` | `search` build plugin and the headless `createSearch` browser client. | | `@farm.js/core/after` | `after` for short work that starts after the current response finishes. | | `@farm.js/stripe`, `@farm.js/polar`, `@farm.js/autumn` | Dedicated billing integration packages. | | `@farm.js/auth0`, `@farm.js/authjs`, `@farm.js/better-auth`, `@farm.js/clerk` | Dedicated auth integration packages. | | `@farm.js/supabase`, `@farm.js/workos` | Dedicated auth integration packages with generated typed callers. | | `@farm.js/email` | Resend integration and email template helper. | | `@farm.js/jobs` | Jobs integration, `task`, `defineTasks`, Trigger.dev runtime, Inngest runtime. | | `@farm.js/unkey` | Unkey API key integration. | | `@farm.js/integrations/*` | Compatibility re-exports for older imports; new code should use the dedicated packages above. | ## Mental model - Pages and layouts are app UI. - API routes are app-owned HTTP handlers. - API clients are typed callers for app routes. - Integrations are provider or feature packages that can own routes, callers, schemas, providers, config, and lifecycle. - Plugins are framework-level hooks for server requests, rendering, builds, browser hydration, navigation, and runtime behavior. - KV storage uses `getStorage()` for values addressed by string keys. - Application models use an application-owned ORM; schema-backed integrations use `ctx.args.db`. - The current beta `storage.client` config name also accepts a raw integration database client, but that client is not a KV mount. - Cron maps a UTC schedule to an app-owned GET API route; it does not add durable workflow state. --- ## Renderers URL: /docs/renderers Choose React, Preact, Solid, Vue, or Svelte for application UI while keeping FARMJS routing, server APIs, middleware, observability, and deployment. # Renderers Choose React, Preact, Solid, Vue, or Svelte for application UI while keeping FARMJS routing, server APIs, middleware, observability, and deployment. React remains the default; Preact, Solid, Vue, and Svelte are beta renderer adapters. ## Choose a renderer | Renderer | Select it with | Route components | Best fit | | -------------------------------- | -------------------- | ---------------- | ------------------------------------------------------------ | | [React](/docs/renderers/react) | Omit `renderer` | `.tsx`, `.jsx` | Complete FARMJS client API and integration UI support. | | [Preact](/docs/renderers/preact) | `renderer: preact()` | `.tsx`, `.jsx` | Small React-compatible runtime with streaming SSR. | | [Solid](/docs/renderers/solid) | `renderer: solid()` | `.tsx`, `.jsx` | Fine-grained interactive UI with FARMJS server features. | | [Vue](/docs/renderers/vue) | `renderer: vue()` | `.vue` | Vue SFCs, SSR, hydration, and FARMJS server features. | | [Svelte](/docs/renderers/svelte) | `renderer: svelte()` | `.svelte` | Svelte 5 components, runes, SSR, and FARMJS server features. | The renderer controls component compilation, element creation, server rendering, and browser hydration. FARMJS continues to control route discovery, layouts, API routes, middleware, cache and storage, integrations, observability, and deployment output. ## Feature support The first two tables are checked against the renderer descriptors and their server/client exports, so entry-point or capability changes cannot silently leave the documentation stale. <!-- renderer-capability-matrix:start --> | Renderer | Vite entry | Server entry | Client entry | Route module extensions | | -------- | ----------------------------------- | ------------------------------------- | ------------------------------------- | --------------------------------------- | | React | `@farm.js/core/renderer/react/vite` | `@farm.js/core/renderer/react/server` | `@farm.js/core/renderer/react/client` | `.ts`, `.tsx`, `.js`, `.jsx` | | Preact | `@farm.js/preact/vite` | `@farm.js/preact/server` | `@farm.js/preact/client` | `.ts`, `.tsx`, `.js`, `.jsx` | | Solid | `@farm.js/solid/vite` | `@farm.js/solid/server` | `@farm.js/solid/client` | `.ts`, `.tsx`, `.js`, `.jsx` | | Vue | `@farm.js/vue/vite` | `@farm.js/vue/server` | `@farm.js/vue/client` | `.ts`, `.tsx`, `.js`, `.jsx`, `.vue` | | Svelte | `@farm.js/svelte/vite` | `@farm.js/svelte/server` | `@farm.js/svelte/client` | `.ts`, `.tsx`, `.js`, `.jsx`, `.svelte` | | Renderer | SSR | Streaming | Hydration | Component head output | Route updates | Plain functions | | -------- | --- | ------------------------------------------------------------------------------------------------------------------------ | --------- | --------------------- | ------------------------------------- | --------------- | | React | Yes | Node (Web on edge targets) | Yes | Framework metadata | Reconciles | Yes | | Preact | Yes | Node + Web | Yes | Framework metadata | Reconciles | Yes | | Solid | Yes | Node + Web | Yes | Framework metadata | Remounts ([why](#re-render-behavior)) | Yes | | Vue | Yes | Node + Web | Yes | Framework metadata | Reconciles | Yes | | Svelte | Yes | Buffered ([test](https://github.com/farming-labs/farm.js/blob/main/packages/farm-svelte/src/__tests__/renderer.test.ts)) | Yes | Native + framework | Reconciles | Yes | <!-- renderer-capability-matrix:end --> “Component head output” means markup emitted from inside the renderer's component model. All five renderers support FARMJS static metadata; Svelte additionally carries `<svelte:head>` output through its server adapter. “Route updates” describes what happens when FARMJS hands an existing browser root a freshly materialized route tree, not ordinary reactive updates inside a mounted component. The higher-level framework features below use those adapter primitives: | Capability | React | Preact | Solid | Vue | Svelte | | ------------------------------------------------ | ------------------------ | ------------------------------- | -------------------- | -------------------- | -------------------- | | File pages and nested layouts | Available | Available | Available | Available | Available | | Loading, error, not-found, and slot files | Available | Available | Available | Available | Available | | Static metadata and favicon configuration | Available | Available | Available | Available | Available | | API routes and generated typed API clients | Available | Available | Available | Available | Available | | Server functions, middleware, cache, and storage | Available | Available | Available | Available | Available | | Observability and production Node output | Available | Available | Available | Available | Available | | Basic create-app starter | Available | Available | Available | Available | Available | | Better Auth create-app starter | Available | Native | Native | Native | Native | | Router state and programmatic navigation | Available | Through `preact/compat` | Solid binding | Vue binding | Svelte store | | Callable actions and server queries | Available | Through `preact/compat` | Solid binding | Vue binding | Svelte store | | Client theme and i18n state | Available | Through `preact/compat` | Solid binding | Vue binding | Svelte store | | Renderer-specific `Link` and form components | Available | Through `preact/compat` | Not yet | Not yet | Not yet | | Programmatic UI routes | Available | Compatibility surface | React-oriented today | React-oriented today | React-oriented today | | Markdown/MDX visual routes and docs adapter | Available | Compatibility surface | React-oriented today | React-oriented today | React-oriented today | | Generated JSX metadata images | Available | Compatibility surface | React-oriented today | React-oriented today | React-oriented today | | React Server Components and optimized boundaries | Available experimentally | Not applicable | Not applicable | Not applicable | Not applicable | | Other integration UI providers and starters | Available | Provider-specific compatibility | React-oriented today | React-oriented today | React-oriented today | Follow the focused compatibility notes for [Preact](/docs/renderers/preact#react-compatibility), [Solid](/docs/renderers/solid#current-boundaries), [Vue](/docs/renderers/vue#current-boundaries), and [Svelte](/docs/renderers/svelte#current-boundaries) before choosing a non-React renderer for a React-oriented UI surface. In experimental React Server Components, synchronous page and layout components are rendered through React, including supported server hooks such as `useId()`. A string or variable containing `async` does not make a component asynchronous. Stateful hooks and effects still belong in Client Components. Preact resolves the React-shaped bindings through `preact/compat`. Solid exposes signal-backed getters, Vue exposes refs and computed values, and Svelte exposes readable stores. The underlying navigation, action, query-cache, theme, and i18n transports live in the renderer-neutral `@farm.js/core/renderer-client` entry. ## Native client bindings Import client bindings from the selected renderer rather than importing React hooks: ```ts // Solid import { useAction, useRouter, useServerQuery, useTheme } from "@farm.js/solid/bindings"; // Vue import { useAction, useRouter, useServerQuery, useTheme } from "@farm.js/vue/bindings"; // Svelte import { createAction, createRouter, createServerQuery, createTheme, } from "@farm.js/svelte/bindings"; ``` Actions remain normal typed RPC calls. Solid exposes action state through reactive properties, Vue through refs, and Svelte through the callable action's readable-store subscription. Server queries share FARMJS's existing browser cache, invalidation, deduplication, stale-while-revalidate, focus, and reconnect behavior across all bindings. ## Renderer capability contract Renderer packages advertise streaming support in their descriptor instead of relying on FARMJS to guess from optional runtime exports: ```ts import { defineRenderer } from "@farm.js/core"; export const customRenderer = defineRenderer({ name: "custom", vite: "@example/renderer/vite", server: "@example/renderer/server", client: "@example/renderer/client", capabilities: { streaming: { node: true, web: false, runtimes: { edge: { node: false, web: true }, }, }, reconcilesRerenders: false, functionComponents: false, }, }); ``` The top-level streaming values are the fallback when no runtime override exists and for presets whose runtime is unknown. Local development resolves as `node`; production resolves from the deployment preset. A `runtimes.node` or `runtimes.edge` entry overrides the pair once FARMJS knows the runtime. Existing renderers that support the same primitives everywhere can keep using only the two booleans. | Renderer | Node target | Edge target | | -------- | ------------------ | ------------------ | | React | Node stream | Web stream | | Preact | Node + Web streams | Node + Web streams | | Solid | Node + Web streams | Node + Web streams | | Vue | Node + Web streams | Node + Web streams | | Svelte | Buffered | Buffered | ### Re-render behavior `reconcilesRerenders` states whether re-rendering an existing root diffs the new tree against the live DOM or rebuilds it. Virtual-DOM renderers (React, Preact, Vue) compare the incoming tree with what is mounted. Svelte's FARMJS compatibility root likewise applies a new element description through one mounted reactive root. In both cases, a client navigation keeps matching DOM nodes, focus, and component state. Solid remains a compile-time fine-grained renderer: handing `root.render()` a freshly materialized tree replaces its nodes because there is no virtual DOM to diff. FARMJS therefore does not use ordinary root re-rendering for a shared Solid layout. The client runtime supplies route state to the adapter's optional `renderRoute()` method instead; the Solid adapter keeps the matching layout chain mounted and updates its page slot and params through signals. Changing the layout chain still mounts the new chain, as it does in the other renderers. Renderers that do not declare the field are treated as rebuilding, so nothing silently depends on reconciliation it will not get. The shared renderer conformance suite asserts the behavior each renderer declares, so the flag cannot drift away from what the adapter actually does. Custom fine-grained renderers can implement the same optional route-update contract: ```ts import type { FarmRendererClientRoot, FarmRendererRouteState } from "@farm.js/core/renderer"; interface CustomRoot extends FarmRendererClientRoot { renderRoute(state: FarmRendererRouteState): void; } export function hydrateRoot( container: Element, element: unknown, initialRoute?: FarmRendererRouteState, ): CustomRoot { // Establish native reactive bindings from initialRoute during hydration. // Later navigations call root.renderRoute(nextRoute). } ``` `layouts` arrive outermost first with stable route patterns, `page` is the innermost route slot, and `params` is the current route-param snapshot. `element` is the fully composed fallback tree, while `wrap()` reapplies framework-owned outer wrappers such as integration providers. Renderers that omit `renderRoute()` continue through `render(element)` unchanged. ### Function components `functionComponents` states whether the renderer can render a plain function component: one that takes props and returns an element tree rather than a component built by the renderer's own compiler. FARMJS gates integration provider components on this capability. All official renderers support synchronous plain function components. React and Preact handle them through their native element model, while Vue and Solid materialize their returned element tree in the adapter. Vue and Svelte also need their adapters to distinguish a plain function from one of their own components, because both are functions at runtime: a native Vue functional component takes `(props, { slots, attrs, emit })` and returns VNodes, and a compiled Svelte component is a function too. Their adapters call only the functions FARMJS marked and render every other function as a native component, so Vue functional components keep their slots, attrs, and emit. FARMJS resolves the ambiguity at build time rather than guessing. A provider component in a production build must be an importable module reference, so the module's extension answers the question: a `.svelte` provider under the Svelte renderer is a Svelte component, while a `.tsx` one is a function component. The check uses the extensions the renderer itself declares in `componentExtensions`, not the resolved set, which always includes `.ts`, `.tsx`, `.js`, and `.jsx`. Components that are not renderer-compiled are marked so the adapter calls them instead of instantiating them. Async plain function components are not part of this contract. Official adapters reject them with an actionable error instead of rendering a promise, an empty string, or `[object Object]`. Resolve the data before creating the element tree or use the renderer's native asynchronous primitives. The field defaults to `false`, so a renderer whose adapter has not been taught to handle function components fails with a clear error naming the renderer rather than rendering something broken. FARMJS builds the production client and SSR graphs in parallel by default. If a renderer's compiler plugin uses process-global mutable caches, set `buildConcurrency: "serial"` on its descriptor. The official Vue renderer does this because `@vitejs/plugin-vue` shares SFC descriptor and script caches between plugin instances. A renderer advertising `node` streaming for the active runtime must export `renderToPipeableStream()` from its server entry. A renderer advertising `web` streaming must export `renderToReadableStream()` returning a WHATWG `ReadableStream`. FARMJS validates those declarations when the development server renderer starts and again in the generated production runtime. Production still checks the function before calling it, but only a primitive enabled by the resolved descriptor can be selected. This keeps the declaration and runtime export in agreement instead of treating an accidental export as support. FARMJS uses buffered `renderToString()` when neither capability is enabled. Descriptors without a `capabilities` field remain buffered for compatibility. ## Renderer-neutral server code Keep product and server behavior outside the component runtime whenever possible: ```ts import { createEndpoint } from "@farm.js/core"; import { createServerFn } from "@farm.js/core/server-fn"; import { z } from "zod"; const input = z.object({ name: z.string().min(1) }); const greet = createServerFn({ input, async handler({ input }) { return { message: `Hello, ${input.name}` }; }, }); export const POST = createEndpoint( "/api/greeting", { method: "POST", body: input }, async ({ body }) => greet(body), ); ``` React, Preact, Solid, Vue, and Svelte components can call this endpoint through the same generated client. Database access, secrets, validation, cache invalidation, middleware, and the server-function handler remain on the server. ## Switch renderers deliberately The renderer option is application-wide. Do not mix React, Preact, Solid, Vue, and Svelte route components in the same route tree. Share server modules, schemas, API clients, CSS, and plain TypeScript across renderers; rewrite component and client-state code using the selected renderer's native primitives. The Basic and Better Auth templates support every renderer directly: ```bash pnpm create @farm.js/app my-auth-app --template better-auth --renderer vue --typescript ``` Other integration starter templates currently target React. Add their renderer-neutral server integration to a native Basic starter when using Preact, Solid, Vue, or Svelte. --- ## Preact Renderer URL: /docs/renderers/preact Use Preact components, hooks, SSR, streaming, hydration, and typed FARMJS server calls through the @farm.js/preact adapter. # Preact Renderer `@farm.js/preact` connects Preact JSX compilation, server rendering and streaming, browser hydration, and Prefresh to the FARMJS renderer contract. React remains the default unless the application selects Preact. ## Create an app ```bash pnpm create @farm.js/app my-preact-app --template basic --renderer preact --typescript ``` For an existing Basic app, install the adapter and Preact: ```bash pnpm add @farm.js/preact preact ``` ```ts import { defineConfig } from "@farm.js/core"; import { preact } from "@farm.js/preact"; export default defineConfig({ renderer: preact(), }); ``` ## Pages and layouts Preact routes use `.tsx` or `.jsx`. Layout children use Preact's `ComponentChildren` type: ```tsx import type { Metadata } from "@farm.js/core"; import type { ComponentChildren } from "preact"; import "./globals.css"; export const metadata: Metadata = { title: "FARMJS with Preact", }; export default function RootLayout({ children }: { children?: ComponentChildren }) { return <>{children}</>; } ``` ## Hydrate interactive routes Mark an interactive component with `"use client"`, then use Preact hooks normally: ```tsx "use client"; import { useState } from "preact/hooks"; export function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount((value) => value + 1)}>Count: {count}</button>; } ``` FARMJS renders route HTML with `preact-render-to-string`, uses its Node or Web streaming runtime when the deployment supports streaming, and calls Preact's `hydrate()` to claim the existing DOM. See Preact's [API reference](https://preactjs.com/guide/v10/api-reference/) and [server-rendering guide](https://preactjs.com/guide/v10/server-side-rendering/) for runtime-specific behavior. ## Call FARMJS server code API routes, endpoint schemas, server functions, middleware, cache, storage, and observability are renderer-neutral. Use the generated API client from Preact UI: ```tsx "use client"; import { useState } from "preact/hooks"; import { api } from "../lib/api.generated"; export function Greeting() { const [message, setMessage] = useState("Ready"); const callServer = async () => { const result = await api.greeting.post({ body: { name: "Preact" } }); if (result.data) setMessage(result.data.message); }; return <button onClick={callServer}>{message}</button>; } ``` The endpoint can call a validated `createServerFn`; its handler, database access, and secrets stay in the server bundle. ## React compatibility The renderer uses the official `@preact/preset-vite`, which aliases React and React DOM imports to `preact/compat`. This lets many existing FARMJS client components and hooks run without shipping a second UI runtime. Compatibility for third-party React integration providers still depends on each provider, so verify provider UI before relying on it in production. React Server Components remain React-only. Programmatic UI routes, Markdown/MDX visual pages, the docs adapter, generated JSX metadata images, and integration UI providers should be treated as compatibility surfaces until their Preact paths are covered by dedicated tests. The Better Auth starter includes native Preact routes and forms: ```bash pnpm create @farm.js/app my-auth-app --template better-auth --renderer preact --typescript ``` Other integration starter templates currently target React, so add their renderer-neutral provider code to a native Basic starter. Run the complete example: ```bash pnpm --filter farm-preact-renderer-example dev ``` See the [Preact renderer example](https://github.com/farming-labs/farm.js/tree/main/examples/preact-renderer) and the [renderer support matrix](/docs/renderers). Preact applications can import FARMJS client hooks from `@farm.js/preact/bindings`. The adapter routes those React-shaped hooks through its existing `preact/compat` aliases. --- ## React Renderer URL: /docs/renderers/react Use FARMJS with the default React renderer, including client hooks, streaming SSR, and the experimental AOT compiler. # React Renderer React is the default FARMJS renderer and has the broadest client-feature support. Existing projects do not need a renderer option or an additional adapter package. ## Create an app ```bash pnpm create @farm.js/app my-app --template basic --typescript ``` Omitting `renderer` keeps React active: ```ts import { defineConfig } from "@farm.js/core"; export default defineConfig({}); ``` ## Route components React routes use `.tsx` or `.jsx` files: ```text src/app/layout.tsx src/app/page.tsx src/app/products/[id]/page.tsx ``` ```tsx import type { LayoutProps, Metadata } from "@farm.js/core"; import "./globals.css"; export const metadata: Metadata = { title: "My FARMJS app", }; export default function RootLayout({ children }: LayoutProps) { return <main>{children}</main>; } ``` Add `"use client"` to an interactive component. FARMJS keeps ordinary server-rendered routes out of the browser bundle and hydrates the client boundaries imported by the route. ```tsx "use client"; import { useState } from "react"; export function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount((value) => value + 1)}>Count: {count}</button>; } ``` ## Shared client state `createStore` from `@farm.js/core/client` provides an in-memory React store with typed field selectors. String, numeric, and symbol state keys work with `use`, `get`, `set`, `subscribe`, and direct field accessors: ```tsx "use client"; import { useState } from "react"; import { createStore } from "@farm.js/core/client"; const selected = Symbol("selected"); export function Selection() { const [store] = useState(() => createStore({ 0: "First item", [selected]: false })); const label = store.use(0); const active = store[selected](); return ( <button aria-pressed={active} onClick={() => store[selected].set((value) => !value)}> {label} </button> ); } ``` Use `store.use([0, selected])` to select multiple fields. Subscriptions only notify for changed selected values, including updates through patches, `replace`, or `reset`. Numeric keys follow normal object semantics (`0` names the same property as `"0"`); symbols retain their identity, even when their descriptions match. State contains own enumerable properties, including symbols. String names used by the store API, such as `use` and `set`, remain reserved. The example creates one store per mounted component; share that instance through context when needed. Do not put request-specific server data in a process-global store. This store does not persist data or serialize symbol keys across server/client boundaries. ## AOT compiler Farm's experimental React compiler moves a narrow class of local state-update work from React's runtime render-and-reconcile path into build-time binding metadata. It is opt-in, disabled by default, and only affects the React renderer that enables it. Preact, Solid, Vue, Svelte, and custom renderers do not receive this transform. > The compiler is experimental. `@farm.js/react` is still a beta, and eligibility, generated > output, and options can change between releases. Unsupported components always stay on React. The compiler is intentionally conservative. It only transforms a component when it can prove that the component has one stable host-element tree and that each supported state value maps to known text or attribute targets. If that proof fails, the original component stays on React. ### Why this compiler exists An ordinary local `useState` update asks React to run the component again, produce another element tree, reconcile it with the previous tree, and commit the changed DOM. That general model is needed for dynamic React applications, but it repeats work when a component's structure is fixed and only a few text or attribute values can change. For that safe subset, Farm prepares three things ahead of time: 1. local state cells plus read-only cells for eligible primitive props; 2. the DOM path of every state-driven text or attribute binding; and 3. the state-cell dependencies for each binding. After mount, an eligible local update flushes the changed cells and patches only the affected bindings. A parent update still enters through React, but flat destructured `string`, `number`, `boolean`, `bigint`, `null`, or `undefined` props can reuse the mounted compiled render plan and patch the same dependency graph after commit. React still owns initial rendering, component placement, the parent update, events, SSR, hydration, and unmounting. Identity-bearing props and unsupported prop shapes keep the full React render path. ### Enable the compiler Start from the focused compiler starter when you want the compiler option, shared dark starter UI, a live AOT-versus-React comparison, and a reproducible browser check already wired together: ```bash pnpm create @farm.js/app compiler-app --template react-compiler --typescript ``` You can also clone the standalone [React Compiler Starter](https://github.com/farming-labs/farmjs-react-compiler-starter). Install `@farm.js/react` to select the React renderer with compiler options: ```bash pnpm add @farm.js/react ``` ```ts import { defineConfig } from "@farm.js/core"; import { react } from "@farm.js/react"; export default defineConfig({ renderer: react({ compiler: true, }), }); ``` `compiler: true` is the recommended starting point. It means automatic inference with safe React fallback: ```ts compiler: { mode: "infer", onUnsupported: "fallback", reactivity: "hybrid", } ``` Omitting `compiler` or setting it to `false` disables the transform. ### Experimental AOT compiler Upgrading from an earlier beta? The option used to live at `react({ experimental: { compiler } })`. That form still works as a deprecated alias: it logs a one-time deprecation warning and behaves exactly like the top-level option. Setting both keys is an error. This heading preserves existing links to the compiler guide; use the [AOT compiler](#aot-compiler) section for new links. ### Compatibility and release status `@farm.js/react` is versioned independently from core. A `-beta.N` package version is still a beta; moving the configuration option does not by itself publish a stable package, and the compiler stays experimental until that happens. The first stable release covers `react({ compiler })`, the documented compiler options, and the public `List` component. Inference and annotation mode, hybrid and static reactivity, diagnostics, and reporting all remain supported; the advanced options do not require an experimental flag. The compiler and `@farm.js/react/compiler-runtime` must come from the same installed package version. Generated definitions are an implementation protocol, not a cross-version format. Rebuild and deploy the complete server and browser output together after upgrading. Do not mix cached compiled modules from different package versions. Existing runtime exports remain available; applications should normally write React components rather than hand-author generated definitions. Eligibility and the set of compiled components can evolve between releases. React fallback, documented options, and observable application behavior are the compatibility contract, not an exact render count, generated helper name, or benchmark score. Unsupported components continue on React unless `onUnsupported: "error"` deliberately makes them a build error. ### Combine the compiler with isolated hydration The compiler and [isolated client hydration](/docs/configuration#isolated-client-hydration) optimize different parts of the same client component. Isolated hydration keeps the server-owned layout out of the browser graph and gives an eligible client leaf its own root. The compiler keeps React in charge of that root's SSR, hydration, events, and unmounting, then patches compiler-proven state bindings directly after mount. ```ts import { defineConfig } from "@farm.js/core"; import { react } from "@farm.js/react"; export default defineConfig({ renderer: react({ compiler: true, }), experimental: { isolatedClientHydration: "enabled", }, }); ``` Farm's maintained [isolated hydration benchmark](https://github.com/farming-labs/farm.js/blob/main/benchmarks/isolated-hydration/results/latest.md) builds the same route-wide and isolated fixtures with the compiler off and on. Compiler builds must prove that every measured component compiled. The cost guard checks initial transfer, hydration, heap, and repeated state updates separately so a steady-state compiler win cannot hide an isolated root startup regression. ### Configuration API ```ts type ReactCompilerMode = "infer" | "annotation"; type ReactCompilerReactivity = "static" | "hybrid"; type UnsupportedCompilerBehavior = "fallback" | "warn" | "error"; interface ReactCompilerOptions { mode?: ReactCompilerMode; directive?: string; onUnsupported?: UnsupportedCompilerBehavior; reactivity?: ReactCompilerReactivity; report?: boolean; reportFile?: string; } ``` | Option | Values | Default | Purpose | | --------------- | ------------------------------------- | --------------------------- | ------------------------------------------------------------------------ | | `compiler` | `false`, `true`, or an options object | `false` | Disables, enables with defaults, or configures the React-only transform. | | `mode` | `"infer"`, `"annotation"` | `"infer"` | Selects components automatically or only through an explicit directive. | | `directive` | non-empty string | `"use compiler"` | Names the module/function directive used by annotation mode. | | `onUnsupported` | `"fallback"`, `"warn"`, `"error"` | `"fallback"` | Controls what happens outside the current supported subset. | | `reactivity` | `"static"`, `"hybrid"` | `"hybrid"` | Selects complete static dependencies or runtime-narrowed subscriptions. | | `report` | boolean | `false` | Writes a compiler coverage report after a successful production build. | | `reportFile` | project-relative path | `.farm/react-compiler.json` | Changes the report path and enables reporting when provided. | `directive` is valid only when `mode` is `"annotation"`. Invalid compiler, reactivity, or unsupported-component modes and directives configured in inference mode throw a configuration error instead of silently changing behavior. ### Static and hybrid reactivity Both modes keep the compiler's complete state dependency list as the correctness boundary. They also build a reverse dependency index once, so a state update goes directly to its candidate bindings instead of scanning every binding in the component. `"hybrid"` additionally marks compiler-proven, multi-state short-circuit expressions and observes the `CompilerCell.get()` calls made by those direct text, attribute, and style readers after mount. Ordinary bindings stay entirely on the static index. A reader such as `enabled ? activeValue : inactiveValue` subscribes to `enabled` and only the selected value. When `enabled` changes, the reader runs and replaces its subscriptions. The generated binding carries the internal `tracking: "dynamic"` marker; application code never manages it. Structural block dependencies remain compiler-defined, and unsupported expressions still fall back to React; this option does not add a Proxy or make arbitrary JavaScript compiler-safe. `"static"` uses the full dependency list for every update. It is useful as a deterministic comparison and diagnostic mode. `"hybrid"` is the default because it can avoid inactive work while preserving the compiler's safe fallback and React ownership boundaries. Both modes cache the last committed direct-binding value and skip an identical DOM write. When Suspense (or React 19's Activity) temporarily hides a compiled component, Farm retains its pending local state and React-owned text-node snapshots. On reappearance, it synchronizes the current committed props before rebuilding binding dependencies and drains queued updates in order. Hiding is not treated as permanent disposal: host refs and refresh subscriptions are detached, while component-owned state remains available for React to resume. During a production build, identical compiler inputs can reuse results across route discovery, SSR, and client compilation. This build-local cache retains at most 128 entries and 16 MiB of source-key/result payloads (excluding JavaScript object overhead and active compilation work). Results are isolated from each consumer, and the cache is discarded after the build, including failures. Diagnostics and compiler reports still run for every transform. Ordinary development keeps Vite's existing caching behavior; there is no persistent source cache, configuration flag, or added browser runtime payload. ### Component selection #### Automatic inference `compiler: true` and `mode: "infer"` inspect top-level, capitalized function components in application `.tsx` and `.jsx` modules under the project root. Dependencies in `node_modules` are not transformed. Every eligible component is compiled; every unsupported component remains unchanged. Use the built-in function directive to keep a specific component entirely on React: ```tsx export function ReactOwnedEditor() { "use no compiler"; // React always owns this component. } ``` The opt-out is intentionally local. It documents a known ownership boundary without disabling the compiler for the rest of the module. #### Annotation mode Annotation mode is useful for a staged rollout or a strict, reviewed set of components: ```ts renderer: react({ compiler: { mode: "annotation", directive: "use compiler", onUnsupported: "warn", }, }), ``` Place the configured directive inside one component to select only that component: ```tsx export function Counter() { "use compiler"; const [count, setCount] = useState(0); return <button onClick={() => setCount((value) => value + 1)}>{count}</button>; } ``` Or place it at the top of a module to select every eligible component in that module: ```tsx "use compiler"; import { useState } from "react"; export function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; } ``` The directive name is configurable: ```ts compiler: { mode: "annotation", directive: "use optimize", } ``` An explicitly selected component that cannot be compiled produces a warning even when `onUnsupported` is `"fallback"`. Explicit selection should not fail silently. ### Unsupported-component behavior | Value | Behavior | Good fit | | ------------ | ---------------------------------------------------------------------------------------- | ---------------------------------------------- | | `"fallback"` | Keep the original React component. Inferred unsupported candidates are quiet. | Broad automatic experimentation. | | `"warn"` | Keep the React component and emit a diagnostic containing the component name and reason. | Finding missed optimization opportunities. | | `"error"` | Stop the transform/build when a considered component cannot be compiled. | Enforcing a reviewed annotation-mode contract. | For example, an unsupported keyed list can report: ```text [react-compiler] KeyedList: dynamic child structures require React reconciliation; using React. ``` Use `"warn"` while exploring the compiler. Use annotation mode with `"error"` when CI should prove that every explicitly selected component still satisfies the supported contract. ### Compiler coverage report Console warnings are useful while editing, but a report makes compiler coverage visible across the production browser graph where compiled DOM updates run. Enable it without changing selection or fallback behavior: ```ts renderer: react({ compiler: { report: true, }, }), ``` After a successful build, Farm writes `.farm/react-compiler.json`: ```json { "version": 1, "summary": { "modules": 2, "componentsConsidered": 4, "compiled": 2, "fallback": 2, "keyedArrayAppendHints": 1, "keyedArrayFilterHints": 1, "keyedArrayPrependHints": 1, "keyedArrayPositionHints": 1, "keyedArrayReorderHints": 1, "keyedArraySortHints": 1, "keyedArrayMappedRollingWindowChainHints": 0, "keyedArrayRollingWindowHints": 1, "keyedArraySliceHints": 1, "keyedCollectionUpdateHints": 3, "keyedIdentityTargets": 2, "keyedMapLookupTargets": 1, "keyedMembershipTargets": 1, "keyedMapUpdateHints": 1 }, "fallbackReasons": [ { "count": 2, "reason": "dynamic child structures require React reconciliation" } ], "modules": [ { "id": "src/Products.tsx", "compiled": ["ProductRow"], "optimizations": { "keyedArrayAppendHints": 1, "keyedArrayFilterHints": 1, "keyedArrayPrependHints": 1, "keyedArrayPositionHints": 1, "keyedArrayReorderHints": 1, "keyedArraySortHints": 1, "keyedArrayMappedRollingWindowChainHints": 0, "keyedArrayRollingWindowHints": 1, "keyedArraySliceHints": 1, "keyedCollectionUpdateHints": 3, "keyedIdentityTargets": 2, "keyedMapLookupTargets": 1, "keyedMembershipTargets": 1, "keyedMapUpdateHints": 1 }, "fallbacks": [ { "module": "src/Products.tsx", "component": "ProductList", "reason": "dynamic child structures require React reconciliation", "selected": false } ] } ] } ``` `componentsConsidered` counts candidates selected by the active mode. `compiled` counts components using the AOT runtime, and `fallback` counts candidates left on React. `keyedIdentityTargets` counts row bindings that can update by looking up the previous and next key directly. `keyedMapLookupTargets` counts row bindings that can update from changed primitive values in a local native `Map`. `keyedMembershipTargets` counts row bindings that can update from the changed members of a local native `Set`. `keyedCollectionUpdateHints` counts proven native Set/Map mutation sites that can carry their executed keys to the runtime. `keyedMapUpdateHints` counts map calls where the compiler proved that a direct keyed collection can report its changed row indexes while one or more consecutive immutable `map()` stages run. The same counts appear per module. `keyedArrayAppendHints` counts setter sites where the compiler proved a direct keyed array append and can hand the appended suffix to the runtime. `keyedArrayFilterHints` counts concise or single-return block-bodied keyed-array filter sites that can report removed positions. `keyedArrayPrependHints` counts setter sites where the compiler proved a direct keyed array prepend and can hand the new prefix to the runtime. `keyedArraySliceHints` counts direct keyed-array slices whose compiler-safe bounds identify one exact retained interval after runtime validation. `keyedArrayPositionHints` counts compiler-proven native keyed-array insertions, single or contiguous-range removals, single-row replacements, and exact-window replacements with a guarded position. `keyedArrayReorderHints` counts native keyed-array reverse steps whose complete permutation can be validated, including steps after a supported filter/slice prefix. `keyedArraySortHints` counts native keyed-array sort steps whose resulting permutation can be validated without rebuilding keyed rows, including steps after a supported filter/slice prefix. `keyedArrayMappedRollingWindowChainHints` counts rolling steps retained across compiler-proven multi-window same-key map chains. `keyedArrayRollingWindowHints` counts direct keyed-array updates that retain a proven sliced tail and append an incoming suffix. `selected` is `true` when an annotation explicitly requested compilation. Module paths are relative to the project root, and the output is sorted and contains no timestamp, so CI can compare reports without machine-specific noise. Use a different project-relative output path when CI collects artifacts elsewhere: ```ts compiler: { reportFile: "artifacts/react-compiler.json", } ``` Report paths cannot be absolute or escape the project root. Reporting is observability only: it does not make unsupported components fail. Use `onUnsupported: "error"` when failure is the desired policy. ### Current supported contract The current compiler deliberately supports a smaller subset than general React. A component must satisfy all of these rules: | Area | Current supported shape | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Component discovery | A top-level, capitalized function declaration, function expression, or arrow component in application `.tsx` or `.jsx`. | | Function shape | Synchronous, non-generator, non-generic block body with zero parameters, one props identifier, or object props destructuring without computed keys. | | Props | Object destructuring supports shorthand names, aliases, defaults, nested object and array patterns, and rest. Referenced primitive values join the dependency graph after mount. | | Body | Top-level `useState` declarations, compiler-safe derived values and named handlers in any order (a derived value cannot read a later state), then one unconditional JSX return. | | State | `const [value, setValue] = useState(initial)`, including lazy initializers, multiple cells, and queued functional updates. | | Root | Exactly one lowercase host JSX element such as `button`, `section`, `input`, or `div`. | | Tree | A statically known host tree around eligible conditional, keyed-list, compiled keyed-row, and component-island boundaries. | | Text bindings | State-driven text in a leaf host element. | | Attribute bindings | Basic attributes, controlled form properties, and individual properties in one inline `style` object. | | Events | Inline handlers and `const` or function-declaration handlers, including async ones, plus inline synchronous events in eligible host-only keyed rows. | | Conditional blocks | Logical/ternary host branches in dedicated containers or direct ranges among static host siblings, including the component root. | | Keyed lists | Item-keyed maps and imported `List`, including safe collection pipelines; eligible rows use direct bindings and recursive host-only conditions. | | Range siblings | Stable host siblings beside ranges may patch leaf text, attributes, `className`, and individual inline style properties. | | Component islands | Stable imported or module-level component identifiers with explicit compiler-safe props and no JSX children, spread, `ref`, or `key`. | This component is eligible: ```tsx "use client"; import { useState } from "react"; interface StatusButtonProps { initial?: number; label: string; } export function StatusButton({ initial = 0, label: title }: StatusButtonProps) { const [count, setCount] = useState(initial); const [active, setActive] = useState(false); const visibleCount = Math.max(0, count); const statusClass = active ? "active" : "idle"; const visibleLabel = `${title}: ${String(visibleCount)}`; function update() { setCount((value) => value + 1); setActive((value) => !value); } return ( <button aria-pressed={active} className={statusClass} data-count={visibleCount} onClick={() => update()} style={{ opacity: active ? 1 : 0.6 }} > {visibleLabel} </button> ); } ``` The compiler records separate dependencies for `count` and `active`. Changing `count` does not reevaluate bindings that depend only on `active`, and vice versa. Derived values are expanded into the generated bindings, so they do not create a runtime scope or force a component rerender. They may use literals, props, state, operators, optional/member access, conditionals, templates, earlier derived values, and a small call whitelist. The whitelist contains `Boolean`, `Number`, `String`, and `Math.abs`, `ceil`, `floor`, `max`, `min`, `round`, `sign`, and `trunc`. Safe keyed collections additionally accept the non-mutating pipeline described below. A name is not treated as built-in when the component shadows it. Outside that pipeline, application helpers, prototype methods, `Math.random`, optional calls, assignments, identity-bearing object or array literals, functions, JSX, constructors, and other unproven expressions still fall back to React. For destructured props, the original component wrapper still performs JavaScript destructuring on every parent render. Defaults therefore apply only to `undefined`, aliases keep their normal local names, and the resolved values are passed to the compiled definition. A named handler can be a `const` function or function declaration, synchronous or async. It is expanded when passed directly to a JSX event or called from that event's inline function, including arguments such as `onClick={() => select(productId)}`. Calling it while producing the event prop, exposing it as a child, using it outside an event, or making it a generator or generic still falls back to React. A handler that can run code later, because it awaits or contains a nested function such as a `setTimeout` callback, reads each state value and prop it uses once, when it starts. Code after an `await` therefore sees the values from the render that created the handler, as it does in React. #### Primitive prop cells Flat destructured props referenced by the compiled render are assigned read-only cells after the component's local state cells. The compiler includes those cell indexes in ordinary text, attribute, style, conditional, keyed-range, and component-island dependencies. For example: ```tsx export function Total({ label, price, quantity, enabled }: TotalProps) { const [discount, setDiscount] = useState(0); const total = price * quantity - discount; return ( <section data-enabled={enabled}> <strong> {label}: {total} </strong> {enabled && <span>Ready</span>} <button onClick={() => setDiscount((value) => value + 1)}>Discount</button> </section> ); } ``` When `label`, `price`, `quantity`, or `enabled` changes to another primitive, React commits the parent update, the compiled wrapper returns its already-mounted element, and the runtime commits the new prop cells. Only dependent targets or blocks refresh. `discount` and a prop change in the same turn share one coherent flush, so `total` observes both newest values. The optimization is deliberately runtime-guarded. If any tracked value is an object, array, function, symbol, React element, or `children`, the definition runs through normal React rendering and reconciliation for that update. Components using an identifier props parameter also retain the existing React prop path; flat destructuring is the current proof boundary. Props used only as `useState` initializers keep React's normal initialize-once semantics and are not turned into live state replacements. No extra option or annotation is required. Stateful styles use one inline object literal. The compiler creates a separate binding for each state-dependent camelCase property or CSS custom property, so changing `opacity` does not rewrite an unrelated `width`. Style spreads, methods, computed names, and conditional whole objects remain on React because their final property set or precedence can change. ### Conditional DOM blocks This optimization is automatic for components selected by the compiler configuration. It does not require a conditional component, a new annotation, or another option. The compiler can isolate logical and ternary child structures when their positions are known at build time: ```tsx export function StatusPanel() { const [loading, setLoading] = useState(false); const [enabled, setEnabled] = useState(true); const [updates, setUpdates] = useState(0); return ( <article data-update={updates}> <header>Status</header> {loading && <p>Loading update {updates}…</p>} <section>Stable content · update {updates}</section> {enabled ? <strong>Enabled at {updates}</strong> : <span>Disabled at {updates}</span>} <footer> <button onClick={() => setUpdates((value) => value + 1)}>Update values</button> <button onClick={() => setLoading((value) => !value)}>Toggle loading</button> <button onClick={() => setEnabled((value) => !value)}>Toggle status</button> </footer> </article> ); } ``` At build time, Farm records each condition's state cells and prepares a descriptor plus exact text/attribute/style bindings for every host branch. It also counts the static host elements before each range and after the final range. React still creates the complete initial tree during client rendering, SSR, or hydration. After mount, Farm adopts the original container, its static siblings, and any active branches. It does not add a wrapper, comment marker, or hydration attribute. When a state update keeps the same branch, Farm patches only that branch's changed bindings and preserves its DOM identity. When the condition changes, Farm inserts, removes, or replaces only that range. Multiple ranges are evaluated from one state snapshot and committed from right to left, so adjacent empty ranges and simultaneous changes keep their original order. Root attributes and ordinary bindings such as `data-update` above continue to patch the same physical root. The user component and unchanged static siblings do not execute or remount. The compiler-owned path is deliberately narrow: - A nested or component-root lowercase host container may contain one or more eligible direct conditionals separated by statically known host siblings. A nested container needs at least two meaningful children; the existing dedicated one-child optimization handles the smaller shape. - Every other direct child is a lowercase host subtree. Its text and ordinary attributes may use supported bindings, and its event handlers remain React events. Fragments, custom components, or another dynamic structure beside a range keep that container on the React-owned path. - Every non-empty branch has one lowercase HTML root and a statically known host-only descendant tree. Text, attributes, and individual inline style properties may use supported expressions. - Branch events, `key`, custom components, fragments, refs, SVG, `dangerouslySetInnerHTML`, JSX spreads, interactive keyed rows, unsupported direct-child structures, and dynamic text mixed beside nested elements require React ownership. - A ternary may use `null` or `false` for an empty branch. For logical `&&`, a numeric falsy value such as `0` can be visible React output, so the runtime remounts that container through React instead of incorrectly treating it as empty. - The test and every prepared binding must use the same deterministic expression subset as other compiler bindings. A dedicated host container whose conditional is its only meaningful child uses the same branch descriptor and update behavior. It remains useful when a branch needs one fixed mounting location: ```tsx <div>{loading && <p>Loading…</p>}</div> ``` ### Recursive compiler-owned host blocks An eligible compiler-owned branch may contain more eligible host-only blocks: ```tsx <div className="panel-slot"> {open ? ( <section> <header>Inbox</header> <div>{loading && <p>Loading…</p>}</div> <ul> <li>Fixed row</li> {messages.map((message) => ( <li key={message.id}>{message.title}</li> ))} </ul> </section> ) : ( <aside>Closed</aside> )} </div> ``` The outer `open` condition, nested `loading` range, and keyed `messages` range receive globally unique block IDs and separate dependency lists. Updating `loading` touches only its host range. Updating `messages` reuses matching rows, patches their prepared bindings, and applies LIS moves. Neither update reruns the component or replaces the outer `section`. If an outer condition and a descendant change in the same microtask, the scheduler commits the outer block first and suppresses the now-redundant child refresh. Removing an outer branch cleans every descendant subscription before removing its DOM. Reopening it creates the selected host tree from current compiler cells, so updates made while closed cannot become stale work. This recursive path is intentionally host-only. Nested conditional ranges may recurse again, and a nested container may own one or more non-interactive keyed ranges separated by static host siblings. Hooks, custom components, branch events, refs, SVG, fragments, dangerous HTML, interactive rows, or unsupported structure inside an inner keyed row keep React ownership. React still renders the initial client/SSR tree, hydrates it, reports recoverable mismatches, and handles every fallback. Duplicate runtime keys or invalid adopted DOM remount the affected outer container through React; descendant dependencies remain live after that handoff. ### Mixed conditional and keyed ranges A safe host container may interleave both range kinds in their original order: ```tsx <section> <header>Inventory</header> {loading && <p>Loading…</p>} <i>Rows</i> {items.map((item) => ( <article key={item.id}>{item.label}</article> ))} {error ? <strong>Error</strong> : <span>Ready</span>} <footer>End</footer> </section> ``` The compiler emits one mixed-range descriptor rather than competing conditional and list controllers. Each slot records its kind and the number of static host siblings before it. On an update, every condition and collection is read from one compiler-cell snapshot. Conditional slots mount, replace, or remove one host branch; keyed slots reuse their per-key instances and run LIS independently. Reconciliation proceeds from right to left, preserving the exact order when adjacent ranges become empty, grow, or change together. Static header, divider, and footer nodes are never used as synthetic markers and retain their DOM identity. Those structurally stable siblings may still contain stateful values: ```tsx <section> <header className={loading ? "busy" : "ready"}>{title}</header> {loading && <p>Loading…</p>} <i data-count={items.length}>Rows: {items.length}</i> {items.map((item) => ( <article key={item.id}>{item.label}</article> ))} <footer style={{ opacity: enabled ? 1 : 0.5 }}>{summary}</footer> </section> ``` The build records each sibling binding by static segment, sibling position inside that segment, and nested host path. Unlike a live `children[index]` address, that coordinate does not shift when an earlier condition mounts or when an earlier keyed range inserts, removes, or reorders rows. One compiler flush can therefore patch the header, divider, and footer while also reconciling every dynamic range, without rerunning the owner or replacing those siblings. Supported bindings are safe leaf text, ordinary attributes, `className`, and individual properties in one inline style object. Safe host-only conditionals and keyed ranges may recurse inside a mixed branch or row. Every syntax site still receives one globally unique block ID and a parent link, so an affected outer mixed block suppresses redundant descendant refreshes. Removing a branch or keyed row cleans its nested mixed subscriptions before removing DOM. Recreating it reads current state, preventing hidden or removed work from becoming stale. Stateful static-sibling bindings use the same mechanism inside recursively compiled branches and keyed rows; row-local bindings close over the current row descriptor and are rebound when a surviving keyed row receives a new item value. This path requires at least one conditional and one keyed `.map(...)` or `List` range in the same lowercase host container. Every dynamic branch and row must have a statically known host tree and a stable item-derived key. Hooks, custom components, events inside branches or rows, controlled forms, refs, SVG, fragments, spreads, dangerous HTML, duplicate runtime keys, numeric logical output, or an invalid hydration/adoption shape keep or transfer that complete container to React. Parent prop and Fast Refresh changes also remount the container through React so the compiler never retains stale closures or static markup. Static siblings remain plain lowercase host trees; lifecycle-sensitive components, Hooks, refs, controlled forms, events requiring compiler DOM ownership, or unproven expressions retain the existing React fallback. The existing React-owned conditional boundary remains the fallback for supported complex shapes. It can contain event handlers, nested host conditionals, keyed lists, and component islands while still skipping the surrounding compiled component. Unsupported roots or unprovable behavior keep the complete original React component. A parent-prop update, Fast Refresh, invalid adopted DOM, or numeric logical output remounts the affected range container through React before direct updates resume. In every tier, React remains responsible for delegated events, error boundaries, Strict Mode, SSR, and hydration semantics. ### Compiled keyed rows and React list boundaries The compiler automatically recognizes a direct keyed map in a safe, statically known container: ```tsx export function Inventory() { const [items, setItems] = useState(initialItems); return ( <ul> {items.map((item) => ( <li key={item.id}>{item.label}</li> ))} </ul> ); } ``` This dedicated `ul` has one meaningful child and each row is a host-only tree. Farm can therefore prepare the row's host descriptor, key reader, and exact text, attribute, and style bindings at build time. React still creates the initial elements during client render or hydration. After the component mounts, Farm adopts those elements as keyed row instances. Updating `items` then does not execute `Inventory` or its JSX row-render callback. Surviving rows are found by key and their prepared bindings are patched directly. Missing keys remove only their row, and new keys create only their prepared host tree. During a reorder, Farm calculates the longest increasing subsequence (LIS) of the old row positions. Rows in that subsequence stay in place; only the other surviving rows move. LIS is a runtime move planner over compiler-prepared rows, not a claim that the future contents of an array are known at build time. When an update keeps every row and changes only a few positions, such as swapping two rows, Farm compares the new keys with the committed order position by position, confirms that the changed positions only trade rows among themselves, and plans moves over those positions alone. A swap moves two rows without building a set of every key. Every row's bindings are still reread, as React would rerender them. #### Key-directed selection updates A separate state or primitive prop often identifies one active row while the collection itself does not change: ```tsx const [selectedId, setSelectedId] = useState<string | null>(null); <tbody> {rows.map((row) => ( <tr aria-selected={row.id === selectedId} className={row.id === selectedId ? "selected" : ""} key={row.id} > <td>{row.label}</td> </tr> ))} </tbody>; ``` For an exact `===` or `!==` comparison against the same expression used by `key`, the compiler records which state or primitive-prop cell supplies the target. After mount, changing `selectedId` looks up the previously selected key and the next selected key in the existing row-instance map. Only those rows evaluate the affected class, attribute, style, or leaf-text binding. A 20,000-row selection therefore performs at most two row-binding evaluations instead of scanning all 20,000 rows. The component and its `map()` callback still do not rerun. This proof is intentionally narrow. The target must be read only as an operand of strict key comparisons, the binding must have no second reactive dependency, and the target must not also change the collection or its key projection. The runtime accepts only string, number, bigint, or nullish targets. Object, array, boolean, ambiguous, mixed structural, React-owned, nested-block, and unsupported expressions use the existing complete evaluation or React fallback. Missing keys are safe and simply patch no next row. No option or component primitive is required. The compiler report exposes the number of emitted row-binding proofs as `keyedIdentityTargets`. For plain keyed rows using only one target kind, the compiler includes only that kind's target runtime: identity, Set membership, or Map lookup. Identity-only selection does not retain the Set/Map snapshot machinery. These paths keep two target snapshot maps instead of six. Rows combining target kinds, row conditionals, nested host blocks, or structural update hints continue using the complete runtime. This changes bundle and bookkeeping costs, not eligibility, update semantics, or the normal React fallback; there is no new option to enable. #### Key-directed Set membership updates Multi-selection usually keeps several row keys in local state: ```tsx const [markedIds, setMarkedIds] = useState(() => new Set<number>()); <tbody> {rows.map((row) => ( <tr data-marked={markedIds.has(row.id)} key={row.id}> <td>{row.label}</td> </tr> ))} </tbody>; ``` For an exact `localSet.has(rowKey)` binding, the compiler records the local state cell and the row key expression. The runtime snapshots the previous and next members and computes their symmetric difference. Only rows whose membership changed evaluate that binding. Replacing two marked keys in a 20,000-row table therefore touches at most four matching row instances instead of scanning every row. The component and its `map()` callback do not rerun. The proof accepts only a direct `has()` call on one local `useState` value with the exact expression used by `key`. At runtime, targeting requires an ordinary native `Set` containing only string, number, bigint, or nullish members. Set subclasses, proxies, own `has` overrides, object members, different row fields, extra reactive dependencies, React-owned rows, nested blocks, and structural key dependencies do not use this path. If a runtime value fails the native-Set guard, that keyed boundary returns to React before compiled bindings are evaluated. This keeps custom collection semantics and error handling under React ownership. No option or component primitive is required. The report exposes the emitted binding count as `keyedMembershipTargets`. #### Key-directed Map lookup updates Per-row status and metadata often live in a local Map rather than on the row objects: ```tsx const [statusById, setStatusById] = useState(() => new Map<number, string>()); <tbody> {rows.map((row) => ( <tr data-status={statusById.get(row.id) ?? "none"} key={row.id}> <td>{row.label}</td> </tr> ))} </tbody>; ``` For an exact `localMap.get(rowKey)` binding, the compiler records the local state cell and row-key expression. The runtime snapshots the previous and next entries and compares mapped values with `Object.is`. Only keys whose visible lookup result changed evaluate the row binding. Status text, validation messages, progress attributes, classes, and styles can therefore update without evaluating the same lookup for every row. The component and its `map()` callback do not rerun. Snapshot comparison still visits Map entries; this optimization removes the full row-binding and DOM-target scan, not the application's Map construction or the runtime's entry comparison. The proof accepts one local `useState` dependency and a direct `get()` call with the exact expression used by `key`. At runtime, targeting requires an ordinary native `Map` with string, number, bigint, or nullish keys and string, number, bigint, boolean, or nullish values. Map subclasses, proxies, own `get` overrides, object values, different row fields, extra reactive dependencies, React-owned rows, nested blocks, and structural key dependencies do not use this path. A failed runtime guard returns that keyed boundary to React before compiled bindings run. No option or component primitive is required. The report exposes the emitted binding count as `keyedMapLookupTargets`. When a key-directed identity, Set-membership, or Map-lookup refresh selects multiple binding updates, the runtime prepares every selected value and resolves every selected DOM target before committing the batch. A reader failure in a later selected row therefore leaves earlier rows untouched, while an invalid target uses the complete React fallback. Preparation retains the same changed-key set; it does not scan unaffected rows. Single-binding refreshes retain the direct path. #### Producer-side Set and Map deltas The Set-membership and Map-lookup paths above normally compare complete previous and next snapshots to discover which keys changed. Farm can remove that second collection scan when it can prove a compiler-owned immutable functional update: ```tsx setMarkedIds((current) => { const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next; }); setStatusById((current) => new Map(current).set(id, "ready")); ``` The JavaScript written by the application still creates and mutates its fresh collection, so `new Set(current)` and `new Map(current)` retain their normal cost and semantics. At build time, the compiler wraps only proven native `Set.add`, `Set.delete`, `Map.set`, and `Map.delete` calls. The generated metadata contains the keys whose mutations actually executed. At commit, the runtime validates those keys against the previous collection and updates only matching keyed-row bindings instead of walking every old and new entry to rediscover the delta. Concise `new Set(current).add(key)` and `new Map(current).set(key, value)` updaters are supported. A block updater may clone `current` once into a `const`, read `has`, `get`, or `size`, perform native mutations, and return `current`, that clone, or a fresh same-kind collection. Queued functional updates compose. Runtime snapshots use persistent change overlays and compact after a bounded chain, so repeated updates do not build an unlimited lookup history. The proof requires a native collection created by the same local `useState` declaration. Every setter use for that state must be either a fresh same-kind replacement or a proven updater. Direct state mutation, unknown setter results, state or draft escape, draft aliases, shadowed constructors, collection subclasses/proxies, object keys, and object Map values disable the delta path. A failed proof keeps the existing full snapshot comparison; a failed runtime safety check returns the boundary to React before compiled binding reads. This is an internal optimization and adds no component, directive, or configuration. Reports expose the number of wrapped native mutation sites as `keyedCollectionUpdateHints`. #### Mutation-aware same-order updates The compiler can remove the runtime's second full-row scan for a common immutable update: ```tsx setItems((current) => current .map((item) => (item.id === targetId ? { ...item, label: nextLabel } : item)) .map((item) => (item.id === targetId ? { ...item, selected: !item.selected } : item)), ); ``` This needs no option or component primitive. At build time, Farm recognizes a functional setter on the direct `useState` collection used by a compiled keyed map or `List`. The setter may be concise or use a block containing exactly one value-returning `return`, and that returned expression may contain one or more consecutive `map()` calls. Extra updater statements, conditional returns, or a missing return keep complete keyed reconciliation. Every mapper must be an inline arrow whose returning paths are conditional: at least one path returns the original item and another returns a new object that spreads that item. This may be a concise expression or a structured block made from fully returning `if` or `switch` branches plus immutable local `const` aliases. Each alias needs a simple identifier and a compiler-safe initializer. A `switch` needs one `default`, a complete return from every case, and no trailing statements. Consecutive empty case labels may share the next fully returning body; a case that executes anything before falling through, or a final label with no body, is rejected. Its discriminant, case tests, conditions, and replacement values must use the same safe expression subset. The hint runtime is retained only in modules where at least one such call is emitted; direct-only and ordinary keyed builds do not import that capability. ```tsx setItems((current) => current.map((item) => { switch (item.status) { case "draft": case "queued": return { ...item, label: draftLabel }; case "published": return { ...item, label: publishedLabel }; default: return item; } }), ); ``` Every native `map()` still runs and is still O(n). After the complete chain succeeds, Farm compares the committed and final item identities once, validates the final key and position for every changed row, and patches each final row once. Intermediate arrays never receive DOM work. The optimization removes the runtime's second full key-and-binding scan rather than removing the application's map work. Multiple hinted functional updates queued in one compiler flush are combined. Farm preserves each source property lookup and call. It records metadata only after an exact native `Array.prototype.map` succeeds on ordinary dense arrays. A custom method, sparse or subclassed array, unsupported mapper anywhere in the chain, changed key, insert, removal, reorder, relevant second dependency, or failed runtime check uses the existing complete keyed reconciliation and LIS path. Native results and thrown mapper or method errors are preserved. Derived collections, non-functional setters, referenced callbacks, mutable or destructured local declarations, effectful initializers, other intermediate statements, partial or dangling fallthrough, mutating replacements, and other unproven shapes also keep that existing path. This is an optimization hint, not a new correctness contract or a way to bypass React fallback behavior. The compiler report exposes the number of prepared map calls as `keyedMapUpdateHints`. #### Keyed array append hints A direct keyed `useState` array can also avoid rescanning all existing rows for common immutable appends: ```tsx setItems((current) => [...current, nextItem]); setItems((current) => [...current, ...nextItems]); setItems((current) => { return [...current, ...nextItems]; }); ``` At build time, Farm recognizes a concise functional setter or a setter block containing exactly one direct value-returning `return`. Its returned array literal must start with exactly `...current` and have at least one trailing item or spread. The application still creates its normal immutable array. Generated metadata connects that result to the last committed array and records where its appended suffix begins. Queued functional appends form one validated chain. At update time, existing keyed rows already have the same item, key, and index. Farm therefore reads keys, descriptors, and bindings only for the appended suffix, creates only those host rows, and inserts them together with a document fragment. It does not rerun the owner component or touch the existing row DOM. The proof is intentionally narrow. Both values must be native arrays. Middle insertion, removal, direct replacement, a copy with no appended entries, updater blocks with extra statements, conditional returns, or no value-returning `return`, duplicate keys, React-owned or nested host-block rows, row conditionals, and rows whose bindings read the collection itself keep complete keyed reconciliation. Keys that read the collection also prevent the compiler from emitting the hint. A preceding unhinted update, an unrelated dirty dependency, or any failed source/length check also discards the hint. These fallbacks preserve normal React behavior; the syntax does not opt the component into a different correctness model. The compiler report exposes emitted sites as `keyedArrayAppendHints`, and the hinted runtime is retained only when a module emits at least one append or same-order map hint. Adjacent removal and append setters can share the same committed-row proof: ```tsx setItems((current) => current.filter((item) => item.id !== expiredId)); setItems((current) => [...current, incoming]); setItems((current) => current.slice(start, end)); setItems((current) => [...current, ...incoming]); setItems((current) => current.slice(start, end)); setItems((current) => [...current, incoming]); setItems((current) => current.map((item) => (item.id === targetId ? { ...item, label: nextLabel } : item)), ); setItems((current) => current.filter((item) => item.id !== expiredId)); setItems((current) => current.map((item) => (item.id === targetId ? { ...item, label: nextLabel } : item)), ); setItems((current) => [...current, incoming]); ``` The setters still execute in order and still create their normal native arrays. Before changing the DOM, Farm validates the original committed token, every filter or slice survivor, the complete final length, and every incoming key. It prepares all incoming keys, descriptors, bindings, and detached host rows first; only then does it remove rejected rows, update survivor indexes, and append the new suffix in one fragment. Existing survivors are neither rebound nor recreated, and additional adjacent append setters are collapsed into the same final suffix. This keeps the unavoidable native filter or slice work while removing the keyed runtime's second full scan. One or more immediately adjacent safe same-key `map()` setters may run between the filter or slice and the append, after the append, or in both positions. A safe map may also precede the structural removal. Farm carries the original survivor indices or retained interval through those native maps and records each replacement against its committed row. Before touching the DOM it validates the complete dense result, every survivor identity, every changed key and binding, and every incoming row. It then removes only rejected rows, patches only changed survivors, and creates the suffix from its final values. The owner stays mounted, survivor DOM identity is preserved, and the suffix is appended once. Only concise updater results and blocks with one direct value-returning `return` that form one direct chain for the same state array are linked. The row and key must be compiler-owned and index-independent. A reorder in the structural-append chain, an unsupported or non-adjacent map, an unhinted update to that state, another dirty row dependency, collection-reading binding, custom, sparse, or subclassed array, nested or React-owned row, duplicate final key, or reuse of any committed key in the appended suffix keeps complete React reconciliation before a DOM write. A mapped survivor whose key changes also falls back before mutation. No API or configuration is added. Compiler reports use the existing `keyedArrayFilterHints`, `keyedArraySliceHints`, and `keyedArrayAppendHints` counts, and modules without both accepted operations omit the composed runtime. #### Keyed array prepend hints A direct keyed `useState` array can avoid rescanning every existing row when new items are inserted at the beginning: ```tsx setItems((current) => [nextItem, ...current]); setItems((current) => [...nextItems, ...current]); setItems((current) => { return [...nextItems, ...current]; }); setItems((current) => current.filter((item) => item.id !== expiredId)); setItems((current) => [nextItem, ...current]); setItems((current) => current.slice(start, end)); setItems((current) => [...nextItems, ...current]); setItems((current) => current.filter((item) => item.id !== expiredId)); setItems((current) => [nextItem, ...current]); setItems((current) => current.map((item) => (item.id === targetId ? { ...item, label: nextLabel } : item)), ); setItems((current) => current.slice(start, end)); setItems((current) => current.map((item) => (item.id === targetId ? { ...item, label: nextLabel } : item)), ); setItems((current) => [nextItem, ...current]); ``` At build time, Farm recognizes a concise functional setter or a setter block containing exactly one direct value-returning `return`. Its returned array literal must end with exactly `...current` and have at least one leading item or spread. The application still creates its normal immutable array. Generated metadata connects the result to the last committed array and records the prefix length. Multiple hinted prepends queued before one compiler flush form one validated chain. At update time, Farm verifies native arrays, source identity, lengths, and every existing suffix item before changing the DOM. It reads keys, descriptors, and bindings only for the new prefix, creates only those host rows, inserts them before the first existing row, and shifts the stored indexes used by delegated row events. Existing row DOM is neither recreated nor rebound. An immediately adjacent compiler-proven `filter()` or bounded `slice()` may feed one or more of these prepends. Farm carries the original survivor positions or retained interval through the queued setters. Before mutation it validates the committed token, the complete dense final array, every survivor identity, and every prefix key, descriptor, and binding. It then removes rejected rows, inserts the prepared prefix once, and updates survivor event indexes. Surviving rows keep their exact DOM nodes and do not rerun their bindings. One or more immediately adjacent safe same-key `map()` setters may run after the prepend or between the removal and prepend. A safe map may also run immediately before the removal. Farm carries every survivor back to its committed row, validates the complete dense result and all final keys and bindings before mutation, then patches only changed survivors and creates the prefix from its final mapped values. The component owner stays mounted and surviving DOM identity is preserved. This proof requires compiler-owned host rows whose render callback and key do not read the row index. Index-aware rows, collection-derived keys, collection-reading bindings, React-owned or nested host-block rows, row conditionals, middle insertion, direct replacement, updater blocks with extra statements, conditional returns, or no value-returning `return`, a reordered structural chain, an unsupported or non-adjacent map, duplicate final keys, reuse of any committed key in the prefix, custom, sparse, or subclassed arrays, an unrelated dirty dependency, or any failed runtime check keeps complete keyed reconciliation before a DOM write. A mapped survivor whose key changes and a prepend queued after an unhinted update also fall back. Reports expose emitted sites as `keyedArrayPrependHints`; no option or component API is added, and modules without the accepted removal, prepend, and map chain omit its mapped runtime. #### Keyed array slice hints A direct keyed `useState` array can retain a known window without rescanning all surviving rows: ```tsx setItems((current) => current.slice(1_000)); setItems((current) => current.slice(0, -1_000)); setItems((current) => current.slice(2, 8)); setItems((current) => current.slice(-5)); const trimCount = pageSize * pagesToDiscard; setItems((current) => current.slice(trimCount)); setItems((current) => current.slice(visible.start, visible.end)); setItems((current) => { return current.slice(visible.start, visible.end); }); ``` At build time, Farm recognizes a concise functional setter or a setter block containing exactly one direct value-returning `return`. The returned expression must directly call native `slice()` with one or two safe-integer literals or compiler-safe runtime expressions. Identifiers, property reads, side-effect-free arithmetic and conditionals, and safe `Math` calls are supported. User-defined calls, assignments, update expressions, and other effectful forms are not transformed. Farm preserves the original method lookup and argument evaluation order, evaluates each bound once, and preserves the native call, coercion, return value, and thrown errors. Generated metadata records the normalized retained interval only when every evaluated bound is already a safe integer, and links multiple slice or filter updates queued before one compiler flush. At update time, Farm validates the native arrays, committed source, queued lengths, interval, and surviving item identities before changing the DOM. It removes only rows outside the interval, updates stored indexes used by delegated row events, and preserves every surviving element. A slice-only chain does not reread surviving keys, descriptors, or bindings, and the owner component does not rerun. The proof requires compiler-owned host rows whose render callback and key do not observe the row index. Effectful bound expressions, evaluated bounds that are fractional, non-numeric, or otherwise not safe integers, `slice()` without a bound, a literal no-op `slice(0)`, updater blocks with extra statements, directives, conditional returns, or no value-returning `return`, chained updaters, custom slice methods, sparse or subclassed arrays, collection-derived keys, collection-reading bindings, React-owned row structures, nested host blocks, row conditionals, unrelated dirty dependencies, and failed runtime validation keep complete keyed reconciliation. A runtime bound that evaluates to a no-op still preserves the native result and uses complete reconciliation. These checks make the metadata an optional optimization rather than a new behavior contract. No configuration or component primitive is added. Reports expose emitted sites as `keyedArraySliceHints`; slice reuses the existing removal-hint runtime so it does not add another structural runtime combination. #### Keyed array rolling-window hints Queues, logs, charts, and fixed-size feeds often expire a prefix while adding new rows: ```tsx setItems((current) => [...current.slice(1), nextItem]); setItems((current) => [...current.slice(1_000), ...nextItems]); const trimCount = pageSize * pagesToExpire; setItems((current) => [...current.slice(trimCount), ...nextItems]); setItems((current) => { return [...current.slice(trimCount), ...nextItems]; }); ``` At build time, Farm recognizes a concise functional setter or a setter block containing exactly one direct value-returning `return`. The first array entry must spread a direct native `current.slice(bound)`, and the remaining entries must be compiler-safe incoming values. The single bound may be a safe-integer literal or a compiler-safe runtime expression: identifiers, property reads, side-effect-free arithmetic and conditionals, and safe `Math` calls are supported. The application still performs the same slice and array construction. Farm preserves method lookup and argument evaluation order, evaluates the bound once, and records only metadata that connects the final array to its committed source and retained interval. At update time, Farm validates native arrays, the complete metadata chain back to the committed source token, the exact retained tail, every retained item identity, and every final incoming key before changing the DOM. Multiple rolling setters queued before one compiler flush collapse into one cumulative prefix removal. Rows introduced by an earlier setter are created only if they remain in the final suffix. Farm removes the expired committed prefix, updates stored event indexes, preserves every retained element, and creates only that final incoming suffix. Incoming keys are checked against the complete committed window; reusing an expired key takes full keyed reconciliation so React key identity is preserved. Safe same-key maps may be immediately adjacent before, after, or between one or more rolling setters in the same synchronous setter segment: ```tsx setItems((current) => current.map((item) => (item.id === editedId ? { ...item, label: nextLabel } : item)), ); setItems((current) => [...current.slice(trimCount), ...nextItems]); setItems((current) => current.map((item) => (item.id === selectedId ? { ...item, selected: true } : item)), ); setItems((current) => [...current.slice(secondTrimCount), ...laterItems]); setItems((current) => current.map((item) => (item.id === editedId ? { ...item, status: "ready" } : item)), ); ``` Each map may use a compiler-safe nested conditional to update several retained rows. Farm links mapped retained items back to their committed rows, creates the incoming suffix from its final mapped values, and prepares every changed binding and incoming row before the first live DOM write. Unchanged survivors keep their elements, focus, selection, scopes, and delegated-event indexes. One rolling setter lowers through the existing structural append/map runtime. A segment with multiple rolling setters retains cumulative rolling-window metadata plus mapped provenance through an optional runtime dedicated to mapped rolling chains. This adds no public helper, configuration, or always-loaded browser feature. The proof remains intentionally narrow: one compiler-safe slice bound, compiler-owned host rows, and index-independent render and key callbacks. A second slice bound, literal zero, effectful bound expressions, updater blocks with extra statements, directives, conditional returns, or no value-returning `return`, custom slice behavior, sparse or subclassed arrays, queued chains containing a mixed or unhinted intermediate update, collection-reading bindings, index-aware rows, React-owned rows, nested host blocks, row conditionals, a statement or unsupported setter inside the segment, unrelated dirty dependencies, changed mapped keys, and failed runtime validation all keep complete keyed reconciliation. Runtime bounds that evaluate to a fractional, non-numeric, unsafe, or no-op value preserve native results and use that fallback. No new component or option is required. Reports expose each emitted rolling site as `keyedArrayRollingWindowHints`, each rolling step retained across a multi-window mapped chain as `keyedArrayMappedRollingWindowChainHints`, and each safe map as `keyedMapUpdateHints`; unmapped rolling modules retain the smaller optional all-hint runtime. #### Keyed array known-position hints Native immutable array methods can state an exact insertion, removal, or replacement position: ```tsx setItems((current) => current.toSpliced(selectedIndex, 0, nextItem)); setItems((current) => current.toSpliced(selectedIndex, 0, nextA, nextB)); setItems((current) => current.toSpliced(selectedIndex, 0, ...incomingItems)); setItems((current) => current.toSpliced(selectedIndex, 1)); setItems((current) => current.toSpliced(selectedIndex, 25)); setItems((current) => current.toSpliced(selectedIndex, 1, replacement)); setItems((current) => current.toSpliced(selectedIndex, 25, ...replacements)); setItems((current) => current.with(selectedIndex, replacement)); setItems((current) => { return current.toSpliced(selectedIndex, 0, nextItem); }); setItems((current) => { return current.with(selectedIndex, replacement); }); const deleteCount = visibleRows.length; setItems((current) => current.toSpliced(selectedIndex, deleteCount, ...replacements)); // Two same-key windows may be queued before one compiler flush. setItems((current) => current.toSpliced(firstIndex, 25, ...firstRefresh)); setItems((current) => current.toSpliced(secondIndex, 25, ...secondRefresh)); ``` At build time, Farm recognizes concise functional setters or setter blocks containing exactly one direct `return`. The position—and, for `toSpliced()`, delete count—must be a safe-integer literal or compiler-safe runtime expression. Identifiers, property reads, side-effect-free arithmetic and conditionals, and safe `Math` calls are supported. User-defined calls, assignments, update expressions, and other effectful forms are not transformed. `toSpliced()` may insert compiler-safe items with a literal zero delete count, remove one item or a contiguous range, replace exactly one item, or replace a positive window with compiler-safe explicit items or a safe spread; `with()` must replace exactly one item. A literal zero delete count with an explicit pair or a safe spread such as `...incomingItems` selects the batch insertion path when at least two items are produced at runtime. A literal delete count above one with any incoming item, a positive literal count with multiple items or a spread, or any compiler-safe runtime delete count selects exact-window replacement; the spread may evaluate to zero, one, or many items. Farm preserves the original method lookup, evaluates every argument once in its original order, and preserves the native call, return value, coercion, and thrown errors. The runtime records metadata only after the native call proves that the evaluated position is already a safe integer and the evaluated delete count is already a positive safe integer. A custom method or a count that is zero, negative, fractional, non-numeric, or otherwise unsafe still runs normally but takes complete keyed reconciliation. At update time, Farm validates the committed native source, result length, source token, normalized position, clamped removal count, and any incoming key before changing the DOM. For a batch, Farm checks existing-key collisions against the committed row map and keeps a temporary set of only the incoming batch's keys. It does not copy every existing key into that set. Complete source-row validation still runs, and Farm computes every incoming key, descriptor, binding snapshot, and detached host row before mutating the live tree. Duplicate incoming keys or collisions with existing keys therefore take complete reconciliation without a partial insertion. Valid rows are mounted in one document fragment; surrounding elements remain connected and only stored suffix indexes shift. Exact-window replacement with fresh keys removes only the proven old interval after every incoming row is prepared. If the incoming interval has the same length and exactly the same keys in the same order, Farm first evaluates all keys and binding snapshots and resolves every changed target across the complete interval. It then patches only changed bindings in place and updates each stored row object, so later delegated or cached handlers observe the latest data. No descriptor or DOM row is created, and every row keeps its identity, focus, and text selection. Validated same-key row and window refreshes also keep the existing row map and element lookup, and skip redundant cache cleanup after the bindings are patched. Source and key validation still run, and each flush commits the new collection token so subsequent updates use current state. Structural changes retain index rebuilding and cache cleanup. A window may instead grow or shrink while it reorders keys from inside its own removed interval and mixes them with globally fresh keys. Farm prepares all reused binding updates, new descriptors, binding snapshots, and detached rows before the first DOM write. It removes only retired rows, preserves each reused row, batches adjacent new rows in a fragment, updates shifted suffix indexes, and applies LIS only to the reused part of that interval so it moves the fewest connected rows needed by the local permutation. Rows outside the interval are not rerendered or rebound. Multiple length-preserving same-key windows queued before one compiler flush compose into one atomic refresh. Fixed-length queued windows may mix same-key rows with globally new final keys, and both disjoint and overlapping windows are supported. An overlapping position uses the last queued value; intermediate identities are never mounted. Farm validates the complete chain and final key set, using the committed row map directly for key collisions instead of copying every existing key into a temporary set. For length-preserving queued windows, it sorts the window ranges and walks them alongside the rows, without building a set of touched positions or sorting those positions. Overlapping and contained ranges prepare each final position once, in row order. This removes bookkeeping, not the full-chain or untouched-row checks: every row is still validated before preparation. It then prepares every touched key, binding value, DOM target, new descriptor, binding snapshot, and disconnected DOM row before the first write. It patches same-key positions and swaps only final fresh-key positions. Untouched rows retain their identity. Duplicate final keys and keys reused from outside a single removed interval take complete reconciliation before fast-path mutation. A queued chain may also contain disjoint grow or shrink windows. Farm maps every immediate-source position through earlier length changes, proves that the source and final intervals remain disjoint, and prepares all local key reuse, fresh rows, binding updates, cleanup, and per-window LIS moves before changing the DOM. Adjacent windows and empty incoming intervals remain eligible. An overlapping structural window or a key transferred between windows keeps complete reconciliation. A single insertion creates one row at that position. A removal cleans up and removes only the known row or contiguous range while preserving every surviving element. A same-key replacement patches that row in place; a new-key replacement creates and swaps one host row. The owner component does not rerun, and surviving row keys, descriptors, and bindings are not reread. The proof requires compiler-owned host rows whose render and key do not observe the row index. Effectful position or delete-count expressions, runtime positions that are fractional or otherwise not safe integers, runtime delete counts that are zero, negative, fractional, non-numeric, or otherwise not positive safe integers, other `toSpliced()` shapes, updater blocks with extra statements, directives, conditional returns, or no value-returning `return`, unsafe incoming expressions, custom methods, overlapping queued structural windows, unhinted intermediate updates, an existing key moved from outside its local removed interval or between queued windows, duplicate final keys, collection-reading bindings, React-owned rows, nested host blocks, row conditionals, unrelated dirty dependencies, and failed runtime checks keep complete keyed reconciliation. Negative safe-integer positions and counts larger than the remaining suffix use the native method's normal clamping rules. No new option or component is required. Reports expose emitted sites as `keyedArrayPositionHints`. Batch insertion and exact-window replacement select progressively separate optional runtime capabilities, so modules with only single-row operations or batch insertion retain no window-replacement runtime. #### Keyed array reorder hints A direct native reverse states the complete next order without changing keyed row identity: ```tsx setItems((current) => current.toReversed()); setItems((current) => { return current.toReversed(); }); ``` Farm recognizes the concise functional-setter form and a setter block containing exactly one direct `return`. It preserves the original method lookup, native call, returned array, and thrown errors. Metadata is recorded only when the result is an ordinary native array, the executed method is the native `toReversed()` method, and the reorder chain starts at the committed collection. A custom or unavailable method therefore keeps its normal behavior and never enters the fast path. At update time, Farm verifies the committed source token, equal lengths, every source row identity, and the exact reversed result before moving the DOM. The runtime leaves one row in place and moves the other rows through connected `insertBefore()` operations, the minimum `n - 1` moves for a reverse. It does not call row keys, recreate descriptors, reread bindings, or run the generic LIS calculation. Existing elements, handlers, form state, and focus stay attached to their keys. Multiple concise native reorder setters may run before the same compiler flush: ```tsx setItems((current) => current.toSorted((left, right) => left.rank - right.rank)); setItems((current) => current.toReversed()); ``` Each setter still executes in order with normal JavaScript semantics. Farm carries the original committed token through consecutive native `toSorted()` and `toReversed()` results, validates the final array as the exact same set of unique item identities, and reconciles that final order once. Intermediate orders never reach the DOM. For a chain made only of reversals, Farm tracks whether the final order is exactly the committed order or its reverse. An even count validates identity and does not build the generic item map, run LIS, or move a DOM node. An odd count validates reverse and uses the specialized minimum `n - 1` move path. Any sort makes the final order a general permutation, so that path still uses LIS. The same proof also works when two or more native reorder operations are chained inside one concise functional setter or a setter block containing exactly one direct value-returning `return`: ```tsx setItems((current) => current.toSorted((left, right) => left.rank - right.rank).toReversed()); setItems((current) => { return current.toSorted((left, right) => left.rank - right.rank).toReversed(); }); ``` Farm prepares this pipeline at build time and evaluates each property lookup, inline comparator, and native call in its original JavaScript order. The intermediate arrays do not reach the DOM. The runtime validates only the final order against the committed collection. A cancelling `current.toReversed().toReversed()` pipeline uses the exact-identity path described above; mixed sort/reverse pipelines apply one LIS-based reconciliation. A compiler-safe `filter()` or bounded `slice()` prefix may run before one or more native reorder steps in the same concise setter or exact one-return block. The filter predicate may itself be a concise expression or an exact one-return block: ```tsx setItems((current) => current .filter((item) => { return item.visible; }) .toSorted((left, right) => left.rank - right.rank) .toReversed(), ); ``` Farm executes every native method normally, carries the structural survivor proof into the final reorder result, and validates the complete operation before touching the DOM. It removes only rejected rows, uses LIS once for the surviving final order, and preserves every surviving element, handler, and form control without recreating descriptors or rereading bindings. Concise or exact one-return filter/slice and reorder setters in the same batch use the same proof. The first proof requires compiler-owned host rows whose render and key do not observe the index. Arguments to `toReversed()`, referenced comparators, computed methods, chains containing a method outside the supported `filter()`/`slice()` prefix and `toSorted()`/`toReversed()` suffix, updater blocks with extra statements, directives, conditional returns, or no returned value, subclassed or sparse behavior, collection-reading bindings, custom methods, structural calls after reordering, an unhinted intermediate update, React-owned rows, nested host blocks, row conditionals, unrelated dirty dependencies, and any identity mismatch use complete keyed reconciliation. No option or component is added. Reports count every compiled structural and reorder step in its existing hint counter; modules without those steps do not retain their optional runtimes. The application runtime must provide `Array.prototype.toReversed`; Farm does not polyfill it. #### Same-key map and reorder pipelines A common data-table update changes one row and immediately restores a sorted order: ```tsx setItems((current) => current .map((item) => (item.id === editedId ? { ...item, label: nextLabel } : item)) .map((item) => (item.id === editedId ? { ...item, rank: nextRank } : item)) .toSorted((left, right) => left.rank - right.rank), ); ``` For a concise functional setter or a setter block containing exactly one direct `return`, Farm can prepare one or more consecutive safe same-key `map()` calls before or after native `toSorted()` or `toReversed()` calls as one update pipeline. JavaScript still performs every map and reorder normally. For two or more accepted maps, the compiler runtime checks each native call as it completes but records replacement lineage only once, by comparing the input and final result of each consecutive map segment. The intermediate arrays need no separate full scan, and the final replacements still point directly to their committed source rows. The complete pipeline then needs one final keyed reconciliation rather than a growing chain of intermediate snapshots. One callback may select several rows with nested expressions, structured early returns, or a fully returning `switch`: ```tsx setItems((current) => current.map((item) => { const matchesReviewed = item.id === reviewedId; if (matchesReviewed) return { ...item, status: "reviewed" }; const matchesEscalated = item.id === escalatedId; if (matchesEscalated) { return { ...item, status: "escalated", priority: 1 }; } return item; }), ); ``` The compiler recursively proves every condition and leaf. Each leaf must return the original item or a compiler-safe object spread of that item, with at least one unchanged and one replacement path. Block bodies may also introduce local `const` aliases with simple identifier bindings and compiler-safe initializers. Mutable or destructured declarations, object/array/function literals, unknown calls, loops, mutation, partial or dangling fallthrough, and any other effectful or ambiguous control flow keep complete keyed reconciliation. Runtime key validation remains atomic, so a replacement that changes its key falls back before the first DOM write. Before changing the DOM, the runtime verifies ordinary dense arrays, exact native methods, the committed collection token, equal lengths, a unique one-to-one source-item match, and the key of every replacement item. It prepares all changed binding values and DOM targets first, runs one LIS reconciliation for the final order, and patches bindings only for rows whose item identity changed. Unchanged rows need no second key or binding read. Existing row elements, delegated handlers, controlled inputs, focus, and text selection stay attached to their keys. Multiple supported map-and-reorder setters queued before one compiler flush compose against the same committed collection and expose only the final state. Safe maps can also appear before, between, or after index-independent filter and slice steps. A final native reorder is optional: ```tsx setItems((current) => current .filter((item) => item.visible) .map((item) => (item.id === editedId ? { ...item, label: nextLabel } : item)) .slice(0, limit) .toSorted((left, right) => left.rank - right.rank), ); setItems((current) => current .map((item) => (item.id === editedId ? { ...item, label: nextLabel } : item)) .filter((item) => item.visible) .slice(0, limit), ); ``` Each callback still evaluates in normal JavaScript order. Farm carries both proofs across the pipeline: which committed rows survived and which surviving items replaced their source items. It validates every survivor and key before the first DOM write, removes rejected rows, performs LIS moves only when a reorder needs them, and patches bindings only for changed survivors. A changed item that a later structural step removes needs no row patch. Structural and map work may also be split across adjacent setter calls in either order: ```tsx // Map, then structural work. setItems((current) => current.map((item) => (item.id === editedId ? { ...item, label: nextLabel } : item)), ); setItems((current) => current.filter((item) => item.visible)); setItems((current) => current.slice(0, limit)); // Structural work, then maps. setItems((current) => current.filter((item) => item.visible)); setItems((current) => current.slice(0, limit)); setItems((current) => current.map((item) => (item.id === editedId ? { ...item, label: nextLabel } : item)), ); setItems((current) => current.map((item) => (item.id === editedId ? { ...item, rank: nextRank } : item)), ); ``` Farm links only consecutive calls to the same setter. A leading map retains its first committed source through later structural work; a terminal map consumes the saved survivor lineage from the preceding filter or slice. Multiple safe steps in either segment may share the proof, and mixed map/structural/map sequences compose. Every result and key is validated before the queued commit touches the DOM. An intervening statement, another setter, an unsupported callback, a changed key, or any failed runtime proof keeps complete reconciliation. That survivor proof may continue through adjacent native reorder setters too: ```tsx setItems((current) => current.filter((item) => item.visible)); setItems((current) => current.map((item) => (item.id === editedId ? { ...item, rank: nextRank } : item)), ); setItems((current) => current.toSorted((left, right) => left.rank - right.rank)); setItems((current) => current.toReversed()); ``` Farm keeps the retained-row positions and same-key replacements attached to the same committed collection. The final update removes rejected rows, patches changed survivors, and applies the resulting order in one validated DOM transaction. Exact reverse chains use the minimum-move reverse path; an arbitrary sort uses one source lookup and LIS. The native setters still run in order. If the calls are not adjacent, target another state value, use an unsupported callback or method, or fail a runtime shape/key check, Farm discards the proof and performs complete reconciliation. A safe map may also be the final pipeline step: ```tsx setItems((current) => current .toReversed() .map((item) => (item.id === editedId ? { ...item, label: nextLabel } : item)) .map((item) => (item.id === editedId ? { ...item, rank: nextRank } : item)), ); ``` Here the maps retain the preceding reorder lineage instead of turning the final value into an unhinted snapshot. An exact reverse uses the direct minimum-move reverse path and patches only changed rows. An earlier sort retains its general permutation proof, so Farm creates one source-item lookup, runs LIS once, and still skips key and binding reads for unchanged rows. Map segments on both sides of a reorder flatten replacements back to the same committed source rows. The same proof can cross adjacent setter calls in one synchronous block: ```tsx setItems((current) => current.toReversed()); setItems((current) => current.map((item) => (item.id === editedId ? { ...item, label: nextLabel } : item)), ); setItems((current) => current.map((item) => (item.id === editedId ? { ...item, rank: nextRank } : item)), ); ``` Farm links only consecutive concise calls to the same setter. Each accepted standalone map keeps the earlier reorder token, so an exact reverse still takes the direct minimum-move path and a sort still performs one validated lookup and LIS. Adjacent compiler-safe filter or slice work switches the chain to the structural proof described above. An intervening statement, a different setter, an unsupported map, or host-backed/nested row structure ends the specialized chain and preserves the existing complete fallback. Map-only components do not retain the optional map-and-reorder runtime. The maps may come first as separate queued setters too: ```tsx setItems((current) => current.map((item) => (item.id === editedId ? { ...item, label: nextLabel } : item)), ); setItems((current) => current.map((item) => (item.id === editedId ? { ...item, rank: nextRank } : item)), ); setItems((current) => current.toReversed()); ``` Farm records safe replacement lineage from the committed collection, then carries it into the following native reverse or sort. This produces the same final result as React's queued functional setters while reconciling the keyed block once. Only adjacent concise calls to the same setter are linked; any statement or unsupported update between them keeps the existing fallback. The mapped lineage can continue through more than one adjacent native reorder setter: ```tsx setItems((current) => current.map((item) => (item.id === editedId ? { ...item, rank: nextRank } : item)), ); setItems((current) => current.toReversed()); setItems((current) => current.toSorted((left, right) => left.rank - right.rank)); setItems((current) => current.toReversed()); ``` Farm still executes every updater and native method in order. Each accepted reverse or sort carries the same committed-row proof forward, so the final value needs one validated keyed reconciliation rather than losing map lineage after the first reorder. A `map().toReversed()` pipeline can start the same adjacent chain. Adjacent compiler-safe filter or slice work changes this into a structural chain while retaining the committed source. An intervening statement, another setter, or an unsupported method ends the chain before later setters are considered. Exact reverse proof can also cross a setter boundary in the same batch: ```tsx setItems((current) => current.toReversed()); setItems((current) => current.map((item) => (item.id === editedId ? { ...item, label: nextLabel } : item)).toReversed(), ); ``` When the maps are followed directly by `toReversed()`, their same-order lineage proves the exact final permutation. Farm validates each mirrored source item and every changed replacement before the first DOM write, then uses the minimum-move reverse operation directly. It does not allocate a second source-item lookup map or run LIS for that case. Further exact reversals toggle the proof: two reversals patch changed rows in committed order with zero DOM moves, while three use the exact reverse path. The runtime now retains that exact identity-or-reverse proof across separate queued setters too. For example, a queued reverse followed by a safe map and another reverse patches the changed row in committed order without moving DOM rows or constructing the generic source map. A sort or any other order ambiguity keeps the general permutation path. The setter may be concise or a block with exactly one direct value-returning `return`. The proof requires every map callback to be inline, synchronous, compiler-safe, and to return the original item on one conditional path and an object-spread replacement on another. Concise expressions and structured blocks containing proven local `const` aliases plus fully returning `if` or `switch` branches are accepted. It requires compiler-owned host rows whose render and key do not observe the index. Referenced callbacks, mutable, destructured, or effectful declarations, other intermediate statements, partial or dangling fallthrough, unconditional replacements, changed or duplicate keys, `thisArg`, maps after structural steps, computed or custom methods, sparse or subclassed arrays, collection-reading bindings, React-owned rows, nested host blocks, row conditionals, unrelated dirty dependencies, or failed runtime validation use complete keyed reconciliation before any fast-path DOM write. No API or option is added. Reports count the prepared map and reorder calls through the existing `keyedMapUpdateHints`, `keyedArraySortHints`, and `keyedArrayReorderHints` fields. Modules that do not contain a supported map-and-reorder pipeline do not retain its optional runtime. Farm does not polyfill `Array.prototype.toSorted` or `Array.prototype.toReversed`. #### Keyed array sort hints A direct native immutable sort can reuse every keyed row while changing only its DOM position: ```tsx setItems((current) => current.toSorted((left, right) => left.rank - right.rank)); setLabels((current) => current.toSorted()); setItems((current) => { return current.toSorted((left, right) => left.rank - right.rank); }); ``` Farm recognizes a concise functional setter or a setter block containing exactly one direct `return`, with either no comparator or an inline synchronous comparator from the compiler's safe expression subset. It preserves the original method lookup, comparator execution, native result, stable-sort behavior, and thrown errors. The native sort still does the comparison work; this optimization removes repeated keyed-row work after the result is known. At commit time, Farm verifies an ordinary dense array whose reorder chain starts at the committed collection, the native `toSorted()` method, equal lengths, and a one-to-one identity match between the committed and final items. It then computes the longest increasing subsequence of the resulting permutation and moves only the rows outside that subsequence. Consecutive concise native sorts and reverses queued before one flush, or chained in one concise updater, share this final reconciliation. Keys, descriptors, and bindings are not reread, the owner component does not rerun, and existing elements, handlers, form state, focus, and text selection remain attached to their rows. This proof requires compiler-owned host rows whose render and key do not observe the row index. Referenced comparators, updater blocks with extra statements, directives, conditional returns, or no returned value, computed methods, custom methods, sparse or subclassed arrays, duplicate item identities, collection-reading bindings, an unhinted intermediate update, structural calls after reordering, React-owned rows, nested host blocks, row conditionals, unrelated dirty dependencies, and failed validation keep complete keyed reconciliation. Native sort/reverse chains may start with compiler-safe `filter()` and bounded `slice()` calls; other chained calls fall back. Reports count each compiled sort step as a `keyedArraySortHints` entry. Sort shares the optional reorder runtime, and Farm does not polyfill `Array.prototype.toSorted`. #### Keyed array filter hints A concise or single-return block-bodied immutable filter on a direct keyed `useState` array can remove rows without rebuilding every surviving row: ```tsx setItems((current) => current.filter((item) => item.id !== removedId)); setItems((current) => { return current.filter((item) => item.id !== removedId); }); setItems((current) => current.filter((item) => { return item.id !== removedId; }), ); ``` At build time, Farm recognizes a concise functional setter or a setter block containing exactly one direct value-returning `return`. The returned expression must directly call native `filter()` with a synchronous, one-parameter predicate from the compiler's safe expression subset. The predicate may be a concise expression or a block containing exactly one direct value-returning `return`. The native `filter()` still runs normally. Its generated wrapper records rejected positions and links queued filters to the last committed array. At update time, Farm validates the native-array chain, result lengths, surviving item identities, and surviving keys before changing the DOM. It then removes only rejected row elements, updates the stored positions used by delegated row events, and keeps all surviving elements in place. Row descriptors and bindings are not recreated or reread, and the owner component does not rerun. The proof applies only to compiler-owned host rows whose render callback and key do not observe the row index. An index-aware row or predicate, collection-derived key, updater or predicate block with extra statements, directives, conditional returns, or no value-returning `return`, custom filter method, sparse or subclassed array, binding that reads the collection, React-owned row structure, nested host block, row conditional, unrelated dirty dependency, or failed runtime validation keeps complete keyed reconciliation. A filter queued after an unhinted update also falls back. These checks make the hint an internal optimization rather than a new behavior contract. Reports expose emitted sites as `keyedArrayFilterHints`; the optional hinted runtime is retained only when a module emits a supported update hint. #### Interactive host rows An otherwise eligible host-only row may contain inline synchronous React events: ```tsx export function Inventory() { const [items, setItems] = useState(initialItems); return ( <ul> {items.map((item, index) => ( <li data-selected={item.selected} key={item.id}> <span>{item.label}</span> <button data-index={index} onClick={(event) => { event.stopPropagation(); setItems((current) => current.map((row) => row.id === item.id ? { ...row, selected: !item.selected } : row, ), ); }} type="button" > Toggle </button> </li> ))} </ul> ); } ``` This uses a hybrid ownership model. React creates or hydrates every row, installs the event props, and performs every structural insert, removal, or reorder. Farm does not call `addEventListener` and does not create an eventful row outside React's Fiber tree. At build time, the compiler replaces each eligible row event prop with a stable keyed proxy and prepares the row's ordinary bindings. When React dispatches the event, the proxy resolves that key's current row instance and passes the latest item and index to the original handler. Replacing `{ id: "a", label: "Alpha" }` with a new object for key `"a"`, or moving it to a new index, therefore cannot leave the handler with the item or index captured by an older map execution. If an update keeps the exact key sequence, Farm patches changed text, attributes, and styles without executing the owner component or map callback. If keys are inserted, removed, or reordered, the internal boundary renders once and lets React reconcile. After that commit, Farm validates and re-adopts the host rows, reapplies every current binding, and resumes direct same-key updates. The reapply step is important because React compares its next props with its previous virtual props, which may predate a direct DOM patch. The initial event contract is deliberately conservative: the row and descendants must still be a statically known HTML host tree; the handler must be inline, synchronous, and free of Hooks, `this`, `super`, and `arguments`. Non-inline or async handlers, custom row components, fragments, refs, SVG, spreads, and other unproven shapes use the React-owned keyed boundary. The same hybrid path is available to an eligible inline host row rendered through `List`. #### Controlled fields in keyed rows An eligible host row can contain ordinary controlled form fields: ```tsx <ul> {items.map((item) => ( <li key={item.id}> <input value={item.label} onChange={(event) => setItems((current) => current.map((row) => row.id === item.id ? { ...row, label: event.currentTarget.value } : row, ), ) } /> <input type="checkbox" checked={item.done} onChange={(event) => setItems((current) => current.map((row) => row.id === item.id ? { ...row, done: event.currentTarget.checked } : row, ), ) } /> <select value={item.priority} onChange={/* the same keyed update pattern */}> <option value="low">Low</option> <option value="high">High</option> </select> </li> ))} </ul> ``` React still creates or hydrates the controls, dispatches `change`, `input`, selection, and composition events, and reconciles any key insertion, removal, or reorder. Farm prepares the `value` and `checked` property bindings at build time. A same-key edit flushes the collection cell, looks up that row instance, and writes only the affected form properties and dependent row output; the owner component and map do not execute again. This is property-based rather than attribute-only. Text inputs and textareas update `.value`, checkboxes and radios update `.checked`, and controlled single or multiple selects update their selected options. The runtime avoids assigning an equal value, captures the focused text control's selection before the compiler microtask, and restores the range after its keyed boundary commits. Composition handlers remain React event props, so IME event order is unchanged. React exposes `event.currentTarget` only while an event handler is running. If a functional compiler-state updater refers to one of its properties, the transform snapshots that property read before queuing the updater. This preserves the common keyed update shown above without retaining or replaying the SyntheticEvent, and it prevents React's controlled-field restoration from changing a deferred `.value` or `.checked` read. The initial optimized contract requires a static input `type` and static `<option>`/`<optgroup>` attributes. File input values, dynamic input types, dynamic option state, `contentEditable`, refs, custom controls, async or non-inline handlers, and a controlled textarea that also has children use the React fallback. Duplicate keys discovered at runtime also switch that mounted list to React. These limits apply equally to automatically detected maps and inline host rows inside `List`. #### Compiler-owned host conditionals inside keyed rows A non-interactive keyed host row may contain logical or ternary host branches. The branches may sit beside static host siblings and may contain deeper safe host-only conditions: ```tsx <ul> {items.map((item) => ( <li key={item.id}> <span>{item.label}</span> <div className="status-slot"> <i>State</i> {item.done ? ( <article> <strong>{item.label} complete</strong> <div>{item.details && <small>{item.description}</small>}</div> </article> ) : ( <span>In progress</span> )} <b>Prepared</b> </div> <section className="details-slot">{item.expanded && <p>{item.description}</p>}</section> </li> ))} </ul> ``` At build time, the compiler prepares the complete lowercase host descriptor for one row, including the tests, static sibling counts, branch factories, text/attribute/style bindings, and recursively nested conditionals. React still renders or hydrates the initial list. After mount, each key owns one row instance and its nested host scopes. A same-key update creates the next lightweight descriptor, patches a surviving branch in place, mounts or removes only a changed branch, and leaves the row and static siblings untouched. The user component and map callback do not rerun. Insertions and removals create or clean only the affected row scopes. Reorders use the same LIS pass as ordinary compiler-owned keyed rows, so a condition change and a reorder queued in one event are committed from the same latest collection. Block IDs remain unique in the component graph, while the runtime scopes repeated row descriptors by their stable key. No marker nodes or extra wrappers are added to SSR output. This compiler-owned tier requires a lowercase host row with no events. Branches may use logical or ternary expressions, multiple known locations, static siblings, nested lowercase host conditions, and safe text, attribute, or inline-style expressions. It applies to automatic keyed `.map(...)` syntax and inline host rows rendered by `List`. Hooks, custom components, events inside a compiler-owned branch, refs, SVG, fragments, JSX spreads, dangerous HTML, controlled inputs inside a conditional, and any unproven shape stay on React. Existing interactive keyed rows keep their React-owned conditional-slot behavior: Farm compares row snapshots and asks React to refresh a changed slot, while React retains event, Fiber, hydration, and structural-list ownership. Duplicate runtime keys or an invalid adopted shape also switch the complete mounted list to React. #### Nested keyed lists inside keyed rows A non-interactive outer row may own one or more inner keyed ranges in known host containers: ```tsx <div> {projects.map((project) => ( <section key={project.id}> <h2>{project.name}</h2> <ul> <li className="heading">Tasks</li> {project.tasks.map((task, taskIndex) => ( <li key={task.id} data-index={taskIndex}> {task.title} </li> ))} <li className="footer">End</li> </ul> </section> ))} </div> ``` The build emits one outer keyed-row descriptor and embeds an inner keyed-range descriptor in that row. At runtime every stable `project.id` owns a separate inner key table for its tasks. Reordering projects applies LIS to the outer list. Reordering one project's tasks applies a second LIS pass only inside that project. A surviving project, task, and every static sibling keep their DOM identity; updating the inner task does not rerun the component or either map callback. When the complete inner key order is unchanged, the nested scope reuses its row table and skips position-map construction and LIS planning. Every key is still validated and every child scope and binding is still updated. Insertions, removals, and reorders use the existing structural path. The same rule applies to keyed ranges beside host-only conditions in nested mixed ranges; it does not expand which row shapes the compiler accepts. The inner scope moves with its outer key and is cleaned when that project is removed. React still renders or hydrates the original nested lists, after which the compiler adopts the existing DOM. An outer and inner update queued together are read from the same latest collection. Duplicate keys at either level, or DOM that cannot be adopted safely, transfer the complete mounted outer list to its original React render. The nested contract accepts automatic `.map(...)` and inline `List` ranges, multiple ranges separated by static host siblings, item-derived keys, and safe text, attribute, and inline style bindings. The outer row may also contain eligible host-only conditional blocks in other known locations. Inner rows remain lowercase, non-interactive host trees. Events, controlled fields, Hooks, custom components, fragments, refs, SVG, JSX spreads, dangerous HTML, and index keys keep the outer row on React's safe fallback. #### Recursive keyed scopes The same analysis continues through every safe keyed host row; there is no special two-level syntax: ```tsx <div> {boards.map((board) => ( <section key={board.id}> <h2>{board.name}</h2> <div> {board.columns.map((column) => ( <article key={column.id}> <h3>{column.name}</h3> <ul> {column.cards.map((card) => ( <li key={card.id}>{card.title}</li> ))} </ul> </article> ))} </div> </section> ))} </div> ``` Build-time recursion assigns a globally unique block ID and parent relationship to every keyed container. At runtime the hierarchy is scoped by stable keys: a board instance owns its column controller, and each column instance owns its card controller. Every controller keeps an isolated key table and performs LIS only inside its own container. A simultaneous board, column, and card rotation therefore preserves all surviving DOM identities and runs one independent move plan at each affected depth without rerunning the component or map callbacks. Removing a parent recursively unsubscribes and removes all descendant scopes. Parent and local updates use the latest complete descriptor tree, and SSR or hydrated DOM is adopted from the outside inward. Duplicate keys or an invalid adopted shape at any level transfer the complete mounted outer keyed boundary to React, where subsequent updates remain live. Recursion does not widen the safety contract. Every optimized row must still be a non-interactive lowercase host tree with an explicit item-derived key. An event, controlled field, Hook, custom component, fragment, ref, SVG, spread, dangerous HTML, index key, or other unproven structure at the deepest level keeps the outer row on React rather than partially compiling an unsafe tree. #### Derived collection pipelines The collection may be prepared through an inline, non-mutating pipeline before the final keyed `.map(...)` or before it is passed to `List`: ```tsx const visibleItems = items.filter((item) => item.visible && item.rank >= minimumRank); const orderedItems = visibleItems.toSorted((left, right) => left.rank - right.rank); const pageItems = orderedItems.slice(offset, offset + pageSize).toReversed(); return ( <ul> {pageItems.map((item) => ( <li key={item.id}>{item.label}</li> ))} </ul> ); ``` The compiler expands those derived locals, records the state cells used by the collection, predicate, comparator, and window arguments, and subscribes the keyed boundary to only those dependencies. When one changes, the pipeline runs once to produce the next collection. The existing keyed-row runtime then reuses surviving rows, patches their bindings, and applies LIS to the new key order. An unrelated state update does not rerun the pipeline or the outer component. The initial pipeline contract supports: - `filter` with one synchronous inline callback using an item and optional index; - `slice` with up to two compiler-safe arguments; - `toSorted` with no comparator or one synchronous inline two-item comparator; - `toReversed` without arguments; and - any sequence of those methods followed by one keyed `.map(...)` or used as `List each`. Callback bodies must contain one compiler-safe returned expression. Assignments, Hooks, async callbacks, spread arguments, calls to unproven helpers or prototype methods, external callbacks, and mutating methods such as `sort`, `reverse`, and `splice` keep the original React component. The compiler does not claim that filtering or sorting is free: it avoids unrelated component execution and React reconciliation, while the necessary collection work still runs when one of its own dependencies changes. Farm leaves these standard methods in the generated JavaScript; it does not inject a polyfill. Applications using `toSorted` or `toReversed` must target runtimes that provide them and include an ES2023 TypeScript library. `filter` and `slice` do not require that newer library. Use the public `List` component when the key should be separate from the row renderer, or when rows are custom components: ```tsx import { List } from "@farm.js/react/list"; export function Inventory() { const [items, setItems] = useState(initialItems); return ( <div className="inventory"> <List each={items} by={(item) => item.id}> {(item) => <InventoryRow item={item} />} </List> </div> ); } function InventoryRow({ item }) { const [expanded, setExpanded] = useState(false); return ( <button onClick={() => setExpanded((value) => !value)}> {item.label}: {expanded ? "open" : "closed"} </button> ); } ``` `List` is an ordinary React component, not compiler-only syntax. It accepts an `Iterable`, `null`, or `undefined`; calls `by(item, index)` to create each React key; and requires its child function to return one React element. It therefore renders correctly when the compiler is off or when this particular use cannot be optimized. The `InventoryRow` example stays in a React-owned keyed boundary because a custom component may contain Hooks, events, context, effects, and local state. The surrounding compiled component can still avoid executing again. Hooks belong inside a row component such as `InventoryRow`, never directly inside the `List` child function or a `.map()` callback. The compiler uses four keyed-list tiers: 1. A dedicated nested host container with one direct map or one `List`, returning a non-interactive host-only row, becomes compiler-owned keyed row instances. Farm patches bindings and uses LIS; eligible outer rows may carry separately scoped nested keyed ranges. 2. A nested or component-root host container with one or more non-interactive keyed host ranges becomes one compiler-owned range block. Farm preserves any static segments and applies the same row descriptors and LIS reconciliation independently inside each range. 3. The dedicated single-list shape with eligible inline events or other React-owned row-local slots keeps React event and structural ownership, while Farm patches same-key row bindings and refreshes only changed conditional boundaries. 4. A safe keyed map or `List` that cannot use an optimized row shape becomes a small React-owned keyed boundary. React reconciles its rows, while the outer user component still avoids rerunning. #### Keyed DOM ranges Static siblings no longer force ordinary host rows onto the React-owned list tier: ```tsx <ul> <li className="heading">Primary</li> {primary.map((item) => ( <li key={item.id}>{item.label}</li> ))} <li className="heading">Secondary</li> {secondary.map((item) => ( <li key={item.id}>{item.label}</li> ))} <li>{primary.length + secondary.length} total</li> </ul> ``` React renders or hydrates the original `ul` without wrapper elements or marker nodes. After mount, the range block partitions its direct element children into static segments and keyed ranges using the build-time shape and current collection lengths. Static header, divider, and footer elements retain their DOM identity. Each range keeps its own key-to-row table and LIS calculation, so an update in one range cannot reorder another range or its surrounding static elements. Stateful text, attributes, `className`, and individual inline styles inside the static host siblings are attached to the range controller through stable segment addresses, so they patch in the same flush as the keyed rows without replacing the sibling. The `ul` above may be the component's returned root. The generated root range block forwards that physical `ul` to the outer binding runtime, so stateful root attributes and styles still patch the same element. A root containing only one eligible map or `List` is supported as a range with no static segments. The compiler does not insert an extra parent merely to host the block. The first range-owned contract is deliberately non-interactive. Every meaningful direct child of the container must be either a lowercase host element or an eligible keyed map/`List`; every range must return a statically known host tree without events or row-local conditional slots. Fragments, components between ranges, interactive or controlled rows, and nested dynamic structures use the existing React-owned boundaries. Parent prop or compatible Fast Refresh changes remount this one range container through React so static markup cannot become stale. Duplicate keys or a mount/hydration shape that cannot be adopted also switch the complete container to React before later updates. The optimized host-row contract is deliberately narrow: - The map must use one synchronous inline callback returning one React element and an explicit item-derived `key`. Its collection may be direct or use the supported non-mutating pipeline. - An array-index key is rejected for compiler isolation because it does not preserve item identity across insertion, removal, or reordering. - The optimized `List` shape uses inline `by` and child functions, a compiler-safe `each` expression, and an item-derived key. - A single interactive list must be the only meaningful child of its nested lowercase host container. Non-interactive host rows may instead occupy one or more ranges separated by direct host siblings, either in a nested container or the component's returned host root. - The row is a statically known HTML host tree. Dynamic leaf text, attributes, controlled host properties, and individual inline style properties are supported. - A dedicated non-interactive map/`List` may contain multiple logical or ternary host branches, including recursively nested branches and branches beside static host siblings. The compiler mounts, replaces, or removes only those host blocks within each keyed row instance. - A dedicated non-interactive row may recursively contain keyed map/`List` ranges in known host containers. Each stable parent key receives an isolated child key table and LIS pass. - Inline synchronous row events use the hybrid React-owned row path. Branch or inner-row events, non-inline or async handlers, file inputs, dynamic form control types or options, custom components, fragments, refs, SVG, attribute spreads, dangerous HTML, and dynamic text mixed beside nested elements stay React-owned. - Unsupported or mutating collection methods, spread children, Hooks in callbacks, and other unproven shapes fall back to normal React. Keyed rows build their DOM-element lookup only when delegated row events need it. Rows without events and rows using ordinary React callbacks skip that lookup; ordinary callbacks still resolve the current item and index through the keyed row table. The lookup is rebuilt when a delegated event definition is adopted, including after compatible Fast Refresh. Stable keys must be unique among siblings and come from the item's identity, such as a database ID. If duplicate keys appear at runtime, Farm remounts that list container through its original React render instead of guessing which row owns the identity. Later updates remain on the React fallback for that mounted list. Interactive fallback handlers use ordinary per-render item/index closures, not ambiguous keyed lookup. Unsupported source shapes also keep React ownership from the beginning. When a parent-prop change or compatible Fast Refresh hands a compiled keyed-row container back to React, Farm checks its direct row elements against the last React adoption. If compiler updates have inserted, removed, replaced, or reordered those elements, Farm remounts that container once before React reconciles it, then adopts the new tree. This preserves the compiled component's local state, but the stale container's DOM identity, focus, selection, and uncontrolled form values reset at that handoff. A current row structure is not remounted; same-key binding updates and later React-owned updates retain their existing controls and user state. Ordinary compiler updates do not perform this handoff check. Once a keyed list or keyed-range container is on React fallback, updates with unique keys reconcile in place. Unchanged form controls, user-entered values, focus, selection, local component state, and DOM identity therefore survive later safe updates. A render containing duplicate keys still forces a complete remount, as does the first unique-key render after it; that recovery reset prevents React from reusing an identity made ambiguous by the preceding duplicate keys. Farm applies the same check when the collection changes through compiler state or through new parent props. Mixed conditional-and-keyed range containers use the same recovery rule for their direct keyed ranges. Once React owns the fallback subtree, branch changes and uniquely keyed list updates keep unaffected controls, focus, selection, child state, and DOM identity. Duplicate or unreadable keys still reset the subtree, followed by one recovery reset when the keys become safe again. The same rule applies recursively to keyed ranges nested inside conditional branches, mixed ranges, and keyed rows: Farm inspects every live nested row before reuse, while any duplicate, unreadable key, or failed descriptor read keeps the complete React reset. For example, changing `[A, B, C, D]` to `[D, A, B, C]` keeps `[A, B, C]` as the LIS and moves only `D`. Reversing four rows needs three moves because the LIS has length one. Insertions and removals still do their necessary DOM work; LIS only minimizes moves among surviving keys. ### React component islands Ordinary child components can stay under React while the compiler owns the surrounding update graph: ```tsx import { Chart } from "./chart"; import { Header } from "./header"; export function Dashboard() { const [count, setCount] = useState(0); return ( <main> <Header title="Dashboard" /> <button onClick={() => setCount((value) => value + 1)}>Count: {count}</button> <Chart value={count} /> </main> ); } ``` `Header` has no dependency on `count`, so a compiled local update leaves it mounted without rendering it again. `Chart` depends on `count`, so the compiler replaces that location with a small internal component boundary and records the exact state dependency. When `count` changes, the button binding is patched directly and only the `Chart` boundary asks React to update. The `Dashboard` function does not execute again. This does not compile or inspect `Chart`. React continues to own its render, Hooks, context, local state, effects, events, lifecycle, reconciliation, errors, SSR, and hydration. Keeping the same component type and boundary position preserves child state across parent-cell updates. Context updates still reach consumers inside the island even when the compiled owner is memoized. The first supported shape is intentionally conservative: - The component name is one direct imported or module-level identifier such as `Chart`. - Props are explicit strings, booleans, or compiler-safe expressions. Inline event handlers may call supported setters. - A prop that depends on local compiler state makes the component a subscribed island. A component with no local-state dependency remains an ordinary, unchanged React child. - The child may internally return `null`, fragments, multiple elements, or other components. - JSX children, spreads, `ref`, `key`, member expressions such as `UI.Chart`, prop-selected dynamic component types, render props, and inline object/array/JSX values fall back to normal React. Direct DOM bindings now use generated private callback refs instead of depending only on sibling indexes. This matters when an earlier React-owned child changes its number of DOM nodes: the compiler still holds the exact target element and cannot accidentally patch a different sibling. The refs produce no `data-*` marker or other extra server markup. ### Composable block graph Conditional, compiler-owned host-conditional, keyed-list, keyed-row, and component-island boundaries are analyzed together. The compiler assigns every boundary a component-wide ID, so IDs remain unique even when several block types share a container or are nested inside one conditional branch. Nested bindings also record the ID of their nearest outer conditional. When one update affects an outer conditional and one or more descendants, the runtime refreshes the mounted outer boundary once and suppresses redundant descendant refreshes for that flush. React then unmounts or replaces the branch normally. Inner boundaries unsubscribe during that unmount, so later updates while the branch is hidden do not target stale components. If the branch appears again, each boundary subscribes again and renders from the latest compiler-cell values. Safe host-only conditions and keyed ranges inside a non-interactive keyed row receive globally unique build-time block IDs, while the runtime creates a separate scope for each stable outer row key. The outer keyed block drives those scopes, so a row can update a nested branch or inner list without a component rerun or a separate global subscription for every row. Each inner list keeps its own key table and LIS pass. The scope moves with the outer row and is cleaned when its key disappears. Eligible inline events use the hybrid React-event path and retain React-owned row-local conditional Fibers. Component islands, Hooks, custom components, interactive or otherwise unsupported nested lists, and other unproven dynamic structure remain on the complete React-owned row path; put Hooks inside a keyed row component. ### What falls back to React | Unsupported shape | Why React keeps ownership | | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Unkeyed/index-keyed maps, unsupported or mutating collection pipelines, or element arrays | Their structure, purity, or item identity is outside the keyed-list contract. | | Unsupported row events/forms, components, fragments, refs, SVG, interactive nested lists, or unsafe conditional branches | Their event, lifecycle, or structure contract requires the React-owned row path. | | Interactive ranges beside siblings or non-host range siblings | The range owner requires a host container and non-interactive host rows. | | Branch events, keys, components, fragments, refs, SVG, interactive rows, or unsupported nested blocks in a compiled conditional range | They require the React-owned conditional path rather than compiler-created hosts. | | Dynamic/member component types, component spreads, refs, keys, or children | Their identity or ownership is outside the first component-island contract. | | Effects or hooks other than the supported `useState` shape | Their lifecycle and ordering must remain under React's hook dispatcher. | | `ref` or `dangerouslySetInnerHTML` | They directly participate in DOM ownership. | | Stateful `children` or `key` bindings outside an eligible boundary | These need structure or identity semantics. | | Conditional style objects, style spreads, methods, or computed names | The final property set or precedence cannot be prepared statically. | | JSX attribute spreads or namespaced attributes | The compiler cannot currently enumerate a stable binding contract. | | Multiple/conditional returns or impure/control-flow statements | The compiler only lowers a single, statically analyzable render path. | | Derived calls, assignments, identity-bearing values, functions, or JSX | Their evaluation timing, side effects, or identity cannot yet be preserved safely. | | Nested, computed, or rest props destructuring | These patterns need additional parameter-shape and identity analysis. | | Object, array, function, symbol, React-element, or `children` prop updates | Identity and child ownership stay on the full React render path. | | Async/generator/generic handlers or named handlers outside JSX events | Their scheduling, identity, or closure semantics are outside the current lowering. | | Async/generator or generic components | These function shapes are outside the current lowering. | | Setters called outside JSX event handlers | The compiler only controls and batches event-driven local updates. | Keys do not make list work disappear. A key identifies the row that survives an insert, removal, or move. On the non-interactive compiler-owned host path, Farm compares those keys, reuses matching row instances, and applies LIS to minimize moves. On the hybrid interactive path, React compares the keyed elements for structural changes while Farm uses the same key to find the newest row data for direct same-key bindings and event dispatch. On the fallback path, React owns the complete row update. Calling a Hook directly inside a list iteration is invalid React because the number or order of calls can change. Put the Hook inside a keyed child component instead; that custom row intentionally uses the React-owned path. ### Build-time transformation The React renderer installs `farm:react-aot-compiler` as a pre-transform. The current implementation is a Babel AST transform, not a Rust compiler pass. It parses application TSX/JSX before ordinary JSX lowering, discovers candidate components, validates the full supported contract, and emits source maps with the transformed module. Local state, setters, derived values, props, and handlers retain JavaScript's lexical scoping, including names that shadow globals such as `Math`, `Map`, or `NaN`. Nested callback parameters still shadow outer bindings. A shadowed global is not treated as a whitelisted built-in call; unsupported usage retains the ordinary React fallback. For each eligible component, the transform conceptually emits a definition like this: ```ts createCompiledComponentWithFeatures( { initialize: (props) => [props.initial], readProps: (props) => [props.label], render: (_props, state) => <button>{state[1].get()}: {state[0].get()}</button>, bindings: [ { kind: "text", path: [], target: 0, dependencies: [0, 1], read: (_props, state) => [state[1].get(), ": ", state[0].get()], }, ], }, [], ); ``` The empty capability array means this component needs only direct bindings. A component with a conditional, keyed list, keyed row, range, or component island receives the matching statically imported feature. Plain keyed rows select a smaller implementation than rows with React-owned conditional slots or recursively compiler-owned host blocks. The selection uses ordinary build-time imports rather than loading code after an interaction, so SSR and hydration remain synchronous. The actual output imports `createCompiledComponentWithFeatures` and only its required feature exports from `@farm.js/react/compiler-runtime` in modules where at least one component compiled. Unused feature exports are tree-shaken from the production browser bundle. Unsupported modules keep their original source and do not receive the runtime import. Runtime features are also part of the Fast Refresh compatibility signature; changing a component into a new structural kind cannot reuse an instance that lacks the new runtime. ### Runtime behavior The generated runtime wrapper is still a React component. Its responsibilities are split as follows: | React owns | Compiler runtime owns | | --------------------------------------------------------- | ---------------------------------------------------------------------------- | | Initial element creation, SSR markup, and hydration | Local compiler-cell values | | JSX event registration and dispatch, including row events | Queuing local setter calls into one microtask | | Parent update scheduling and commit | Primitive prop cells and their dependency-indexed patches | | Identity-bearing and unsupported prop reconciliation | Reusing the mounted render plan for compiler-safe primitive prop updates | | Unsupported trees, hooks, refs, handlers, and lifecycles | Patching stable ref-owned text, attribute, and style targets | | Complex conditional branches, events, and nested blocks | Host-only conditional identity, range placement, bindings, and replacement | | Interactive or conditional-row structure and reorders | Same-key row bindings, latest-item events, and changed conditional snapshots | | React-owned custom or structurally complex keyed rows | Non-interactive host-row identity, bindings, insertion, removal, and LIS | | Component render, Hooks, context, and lifecycle | Refreshing only a dependent React component-island boundary | | Unmounting and the surrounding component tree | Committing prop and local cells together after a parent-driven update | Queued functional setters preserve the event's state snapshot. Two calls such as `setCount(value => value + 1)` are applied in order during the same microtask flush, while reads inside the event still see the previous snapshot. If the final value is `Object.is`-equal to the previous value, no binding is patched. Attribute updates preserve important DOM behavior: - `className` and `htmlFor` map to `class` and `for`; - input and textarea `value`, select values, input `checked`, option `selected`, and element `disabled` use DOM properties; - controlled single and multiple selects update their selected options; - style bindings patch one property, add `px` where React expects it, preserve unitless numbers and CSS custom properties, and clear nullish values; - `data-*` and `aria-*` booleans are stringified; and - nullish values and ordinary false boolean attributes are removed. Focused input and textarea updates capture and restore the current selection around the compiler microtask. Composition events remain React-owned, so IME input keeps its normal event ordering while the resulting value, selection, and dependent bindings are patched together. The server output is ordinary React HTML and contains no compiler marker. React hydrates that markup normally; direct binding updates begin after the component mounts. Eligible conditional and keyed-row containers validate and adopt their already-hydrated child elements, so valid server DOM is not replaced during hydration. Interactive rows keep their React event props throughout this process. A hydration mismatch stays on React's recoverable-error path; Farm adopts only the host shape React committed. In development, React 18 and React 19 Strict Mode can replay mount lifecycles. Eligible keyed-row containers re-adopt their existing DOM after that replay instead of switching permanently to React fallback. A real unmount still clears the host ref, disposes subscriptions, and ignores queued work or stale setters from the removed owner. Direct text, attribute, style, and controlled-input bindings also retain their attached DOM refs during Strict Mode's lifecycle replay. Local updates keep working before a parent renders again, including after hydration. React's ref-detach callbacks clear those references on a real unmount; queued updates and stale setters cannot update a removed component or its replacement. Host conditionals and conditional, keyed, and mixed range containers follow the same replay rule. They re-adopt the existing container, static siblings, and eligible branches or rows, including server-rendered nodes. Nested subscriptions are disposed and re-established during replay; hiding a branch or unmounting the component still disposes its scopes. Unsupported shapes keep the existing React fallback behavior. React-owned conditional boundaries also retain their attached-root registration during replay. Path-based bindings continue to skip those branches, so a nearby text or input update reaches its intended element. Hiding or replacing a branch and real unmounts remove the old registration; disposed owners cannot update the detached nodes or a new component instance. If a host conditional or conditional-range container has already requested React fallback, Strict Mode replay restores its nested fallback subscriptions instead of attempting compiled adoption again. Each committed fallback render also refreshes that subscription set, so a nested block introduced by a parent, local condition, or compatible Fast Refresh becomes live immediately and a removed block stops receiving work. Descendant-only updates remain live, including after hydration, and real unmounts still dispose those subscriptions. The existing fallback eligibility rules are unchanged. Subscription discovery follows only the branch React actually rendered: hidden branches are not evaluated, and their descendants remain unsubscribed until that branch is shown. Refreshing the active set keeps listeners whose block IDs did not change, removes only stale IDs, and subscribes only newly mounted IDs instead of rebuilding every listener after each render. Host conditional and conditional-range containers keep a stable key after entering React fallback when their descendants are either React-owned or conditional-only compiled blocks. Later updates preserve unchanged form controls, user-entered values, focus, selection, and React child state. The first fallback still resets DOM ownership. A keyed descendant retains the full reset path for safe recovery, including duplicate-key recovery, while removing a branch still follows normal React unmount behavior. During development, compiled components receive a module-and-component identity plus a state-layout signature. A compatible Fast Refresh replaces the compiled definition while retaining the React component type and its local cells. An interactive keyed boundary lets React commit the refreshed event layout before Farm re-adopts it, and keyed proxies read the current definition, so a refreshed handler cannot keep executing its older closure. Compiler-owned host conditionals, keyed rows, and conditional, keyed, and mixed ranges also move their update subscription when the refreshed definition assigns that mounted boundary a different internal block ID. Later local updates cannot remain routed to the previous definition. Compiler-owned host conditionals, keyed rows, and conditional, keyed, and mixed ranges also pass through React once when their definition changes, then Farm re-adopts them. This refreshes their static markup even when binding values are unchanged while preserving matching branch, range, and keyed DOM identity, except for the stale keyed-row container handoff described above. React-owned conditional boundaries move their retained root registration with the same ID change, so neighboring path-based bindings continue to skip the branch React preserved. Interactive keyed rows validate the complete refreshed row set before Farm reapplies bindings; a later invalid row therefore returns the container to React without first patching an earlier row. Conditional, keyed, and mixed range owners likewise validate every static binding target before applying any binding update, so a later invalid target cannot partially change an earlier static sibling before React takes over. They also evaluate and normalize every static binding value before the first DOM write, so a later throwing reader reaches the nearest React error boundary without exposing an earlier sibling update. An ordinary surviving keyed row likewise evaluates and normalizes every binding in that row before updating its binding cache or the DOM. A later throwing reader therefore leaves that row untouched; single-row refreshes with one binding retain the direct single-binding path. Stable multi-row refreshes whose row structure is unchanged additionally prepare every row's binding updates before committing any row, so a reader failure in a later row leaves earlier rows untouched. Single-removal refreshes with multiple static survivors use the same preflight before committing survivor bindings or detaching the removed row. Other removal-only refreshes with static survivors also preflight the complete surviving row set before detaching any stale row. Conditional branch bindings and keyed-row bindings inside nested keyed or mixed ranges are also read and prepared without DOM writes until every sibling range has passed adoption. A later invalid range therefore cannot partially patch an earlier branch or keyed row before React takes over. If the compiler-owned state layout changes, the identity is not reused and React remounts it instead of preserving incompatible state. If a direct binding evaluation throws, the runtime schedules a React update and rethrows from the component render. This lets the nearest React error boundary handle the failure through React's normal recovery path. ### Safety reasoning The compiler uses fallback as a semantic boundary, not as an error-recovery trick. Generated callback refs keep direct DOM targets stable even when a React component island returns `null`, a fragment, or multiple nodes. React-owned conditional, keyed-list, and component boundaries give complex dynamic structure back to React. Compiler-owned branches and non-interactive rows are limited to complete host-only containers because manually inserted DOM has no React Fiber for events, components, or Hooks. Interactive rows and other rows that require local conditional Fibers therefore never use the manual insertion/removal/LIS path: React reconciles their structure, and Farm only adopts committed host elements for binding patches and row-local snapshot refreshes. Proven non-interactive host conditions are part of the keyed descriptor instead, so their scopes can move safely with the existing LIS path. Unsupported dynamic component types, refs, effects, and other unproven shapes keep React ownership; fallback is the optimization's correctness mechanism. Parent-driven updates remain React updates, but not every update needs a new compiled element tree. For compiler-emitted flat primitive prop cells, render-phase reads use a pending snapshot without mutating committed cells. React can abandon that render safely. After a successful commit, the runtime publishes the cells and refreshes their indexed bindings and blocks. Identity-bearing values skip this reuse path and receive normal React reconciliation. This split keeps prop changes and local state coherent without moving an imperative DOM write into React's abortable render phase. ### Verification and benchmark scope The package and example test suites verify more than generated code: - compiled local updates change the same text and attributes as base React; - one eligible update adds no React render or commit, while the equivalent base component adds one; - server-rendered markup hydrates and remains interactive; - lazy initialization, event snapshots, and batched functional setters are preserved; - multiple state cells update only their dependent bindings; - whitelisted calculations, per-property styles, handler wrappers, textarea/select/checkbox properties, and multiple-select values update without rerunning the component; - Strict Mode mounting, queued-unmount cleanup, bubbled events, controlled input selection, and composition events preserve their React behavior; - simultaneous parent-prop and compiled-local updates remain coherent; - flat primitive prop transitions patch direct bindings and host conditionals without rebuilding the compiled render plan, preserve focused selection and DOM identity, and match React across deterministic string, number, boolean, and nullish transitions; - object and function prop identities retain the full React render path, while SSR hydration and Strict Mode preserve the primitive prop optimization afterward; - nested fallback blocks read one coherent render snapshot, and an abandoned concurrent render cannot publish prop cells or replace the last committed compiled element; - host conditional and conditional-range fallbacks rebind changing descendant listeners in static and hybrid modes after mount or hydration, drop removed listeners, and dispose the active set on unmount; - fallback subscription discovery skips hidden direct and nested branches in static and hybrid modes after mount or hydration; - stable fallback descendants retain their existing listeners across committed host-conditional, conditional-range, and mixed-range updates; - compatible Fast Refresh preserves state, while binding errors reach React error boundaries; - hydration mismatches follow React's recoverable-error path and remain interactive; - compiler-owned conditionals preserve a same-branch DOM instance, patch nested text, attributes, styles, and focused input selection, replace only a changed branch, and fall back for numeric logical output or any unproven structure; - 3,000 deterministic compiler-owned conditional transitions produce the same host output as normal React while the compiled owner stays at one execution; - conditional DOM ranges preserve an exact component root and every static sibling, mount adjacent empty slots in source order without marker nodes, and commit simultaneous range changes from one state snapshot; - stateful static siblings patch text, classes, attributes, and individual styles through stable segment/sibling/path addresses while conditional mounts and keyed LIS moves change nearby child indexes; their DOM identity survives every update; - 3,000 deterministic nested-range updates and 3,000 component-root-range updates match normal React while unchanged branches, static siblings, and the compiled owner keep their identity; - recursive host scopes preserve an outer branch and static siblings while independently updating nested conditions, deeper conditions, and LIS-keyed ranges; - 3,000 deterministic recursive updates match normal React across outer toggles, nested branch changes, inserts, removals, binding edits, reversals, and rotations without rerunning the owner; - recursive scopes clean queued work on unmount and outer removal, rebind on combined parent-prop and local updates, adopt exact SSR output, recover after hydration mismatches, and keep React fallback live after duplicate runtime keys, while nested binding failures reach React error boundaries; - object, array, and nullish state transitions match normal React across 3,000 deterministic randomized updates; - automatic keyed maps and explicit `List` boundaries preserve keyed DOM nodes and stateful row identity across inserts, removals, updates, and reorders without rerunning the outer component; - compiler-owned host rows patch text, attributes, and styles in place, preserve focus and text selection, use the LIS minimum for measured rotations and reversals, and remount through React when runtime keys are duplicated; - mutation-aware keyed `map()` updates compose consecutive safe stages, compare the committed and final row identities once, patch each final same-key row once, compose queued hints across multiple keyed boundaries, ignore unrelated state in the same flush, and reject key changes, custom methods, sparse or subclassed arrays, or mixed unhinted updates into the complete reconciliation path; - compiler-proven immutable Set/Map updaters carry exact native mutation keys across queued updates, compact long persistent snapshot chains, and fall back before binding reads for unsafe keys, values, collections, or ownership; - 2,000 deterministic hinted Set mutations and 2,000 deterministic hinted Map mutations match normal React while evaluating only changed present-row keys; - a 2,048-row instrumentation test changes one item through two maps with one key read and one binding read; separate stages that change two rows read exactly those two keys and bindings; and 2,000 deterministic queued two-map updates match normal React with one compiled owner execution; - keyed DOM ranges preserve static siblings around multiple lists, support adjacent empty ranges and exact component roots, apply LIS independently per range, and remount the complete container through React when keys or parent-driven static markup invalidate adoption; - interactive host rows keep React event propagation and `currentTarget`, resolve the latest item and index after same-key replacements and reorders, let React own structural commits, and resume direct binding patches without stale virtual-prop output; - editable keyed rows preserve input and row identity, focused selection, IME ordering, checkbox, radio, textarea, and select state while direct same-key edits add no owner or map executions; - interactive rows stay coherent across combined parent/local updates, Strict Mode, compatible Fast Refresh, recoverable hydration mismatches, and an unmount before the compiler microtask flush; - row-local host conditionals refresh only changed keyed slots, remain coherent with inline events and parent updates, preserve row identity through React reorders, switch duplicate keys to React, route failures through error boundaries, and clean subscriptions on unmount; - non-interactive keyed rows own multiple and recursively nested host conditionals, patch a surviving branch without a React commit, combine condition changes with LIS reorders, preserve row, branch, and static-sibling identity, and clean every nested scope when a row disappears; - 2,000 deterministic compiler-owned row/branch/order transitions match normal React while the compiled owner remains at one execution, with additional Strict Mode, parent/local, error-boundary, duplicate-key, queued-unmount, SSR, and recoverable-hydration coverage; - nested keyed rows apply independent outer and inner LIS passes while preserving project, task, and static-sibling DOM identity, including simultaneous updates at both levels; - 2,500 deterministic two-level reorder, insert, remove, binding, and boolean transitions match normal React while the compiled owner remains at one execution, with additional Strict Mode, parent/local, duplicate-inner-key, Fast Refresh, queued-unmount, SSR, and hydration coverage; - recursive keyed scopes apply independent LIS passes at board, column, and card depth while preserving every surviving row and static sibling; - 3,000 deterministic three-level reorder, insert, remove, binding, and boolean transitions match normal React while the compiled owner remains at one execution, with additional Strict Mode, parent/local, deepest-duplicate-key, error-boundary, Fast Refresh, queued-unmount, SSR, and hydration coverage; - mixed conditional/keyed containers match normal React across 3,000 deterministic branch, reorder, insertion, removal, binding, and recursively nested list transitions, with additional Strict Mode, simultaneous parent/local, duplicate-key fallback, error-boundary, queued-unmount, SSR, and recoverable-hydration coverage; fallback state-preservation coverage also spans static and hybrid reactivity, nested and component-root ownership, uncontrolled controls, focus, selection, local child state, duplicate keys, and unique-key recovery; - component islands update only dependent children, preserve child-local state and context, route failures through React error boundaries, hydrate in Strict Mode, and safely drop queued updates after unmount; - stable ref targets remain correct when an earlier island switches among `null`, one node, and a multi-node fragment; - 1,000 deterministic object, array, and nullish component-prop transitions match normal React; - 1,000 deterministic randomized compiler-owned list operations produce the same ordered output as normal React while the list owner stays at one execution; - 2,000 deterministic updates each for nested and component-root keyed ranges match normal React while every static sibling and surviving row keeps its DOM identity and the owner stays at one execution; - 5,000 deterministic filter, sort, slice, reverse, insertion, removal, and row-update transitions produce the same keyed output as normal React while preserving surviving DOM rows; - 4,000 deterministic interactive data, structure, and event transitions match normal React while the compiled owner remains at one execution; - 2,000 deterministic controlled-form and structural transitions match normal React across text, textarea, checkbox, radio, and select properties while the compiled owner stays at one execution; - 2,000 deterministic row-conditional data and structural transitions match normal React while the compiled owner remains at one execution; - 2,000 deterministic keyed-array appends match normal React; targeted tests require key, descriptor, and binding reads to equal only the appended suffix and cover queued updates, multi-boundary sharing, StrictMode hydration/unmount, and conservative fallback; - 2,000 randomized queued filter-or-slice, append, and map transitions plus 2,000 transitions with maps before the append match normal React, with both structural sources retaining their committed survivor positions through the composed fast path; targeted tests require survivor DOM identity, changed-survivor-only binding writes, mixed filter/slice chains, controlled-input focus and selection, multiple maps, multi-boundary sharing, Strict Mode hydration, nested unmount cleanup, changed-key and removed-key reuse fallback, and atomic validation before mutation; - 2,000 deterministic keyed-array prepends match normal React; targeted tests require key, descriptor, and binding reads to equal only the inserted prefix, preserve every existing DOM row, update delegated event indexes, and cover queued updates, StrictMode hydration, unmount, invalid metadata, custom arrays, and conservative fallback; another 2,000 randomized queued filter-or-slice followed by prepend transitions match normal React while targeted tests preserve survivor identity, controlled-input focus and selection, and atomic fallback; 2,000 additional randomized removal, prepend, and same-key map transitions verify changed-survivor-only binding writes, final mapped prefix creation, Strict Mode hydration, external mutation, and changed-key fallback; - 2,000 deterministic queued keyed-array slices and 1,000 randomized runtime-bound slices match normal React; compiler tests cover literal and compiler-safe runtime bounds while rejecting calls and mutations; targeted tests require zero surviving key, descriptor, and binding reads, preserve focused controlled-input identity and selection, update delegated event indexes, and cover native evaluation and coercion, unsafe evaluated bounds, queued slice/filter chains, Strict Mode hydration, unmount cleanup, custom methods, proxies, and conservative fallback; - 250 committed fixed-bound, 1,000 randomized runtime-bound, 1,000 randomized queued, 2,000 randomized mapped single-window, and 2,000 randomized mapped multi-window commits match normal React; targeted tests require work to equal only the final incoming suffix plus changed committed survivor bindings, preserve retained DOM identity, update delegated indexes, preserve controlled focus and selection, and cover changed mapped keys, unsafe evaluated bounds, reused or discarded intermediate keys, custom slices, mixed queued chains, collection-dependent rows, Strict Mode hydration, React 18/19, and unmount cleanup; compiler tests accept recursively proven `if` and fully returning `switch` maps, including grouped empty labels, while rejecting effectful leaves, partial or dangling fallthrough, and callbacks that replace every row; - 2,000 deterministic randomized keyed-array removals match normal React; targeted tests require zero surviving descriptor and binding reads, preserve DOM identity, and cover queued filters, unhinted-chain fallback, collection-reading rows, StrictMode hydration, and unmount cleanup; - 2,000 deterministic randomized removals followed by native sorting and reversal match normal React; targeted tests cover filter/slice/reorder pipelines, queued filter-then-sort updates, surviving controlled-input focus and selection, exact DOM identity, custom-method and identity fallback, StrictMode hydration, and unmount-before-flush cleanup; - 2,000 deterministic same-key edits followed by native sorting match normal React; targeted tests require one source-to-final descriptor scan for consecutive maps and one changed-row binding read, preserve every keyed DOM node, compose queued map/sort and map/sort/reverse pipelines, dispatch moved-row events with the newest item and index, preserve controlled-input focus and selection, and cover changed-key, custom-method, and Array-subclass fallback, Strict Mode hydration, and unmount-before-flush cleanup; - 2,000 deterministic mapped structural removals, another 2,000 randomized updates with maps interleaved between filter/slice steps and a final reorder, 2,000 randomized terminal structural-map transitions, 2,000 randomized map-before-terminal-filter/slice transitions, and separate 2,000-transition adjacent-setter runs with map/structural work in both orders match normal React; targeted tests preserve surviving DOM identity and controlled-input focus/selection, patch only changed survivors, and cover multiple terminal maps, adjacency boundaries, changed-key and custom-method fallback, Strict Mode hydration, and unmount-before-flush cleanup; - 2,000 deterministic native reversals or sorts followed by consecutive same-key edits match normal React; targeted tests require exact reversal to use `n - 1` direct DOM moves without a generic source-item map, require ambiguous sorting to use one validated keyed reconciliation, and cover changed keys, custom methods, Strict Mode hydration, and unmount-before-flush cleanup; - another 2,000 deterministic reverse-or-sort operations followed by two adjacent standalone map setters match normal React; compiler tests cover same-state adjacency and conservative boundaries, while targeted tests preserve DOM identity, exact reverse moves, one changed-row binding read, Strict Mode hydration, and unmount-before-flush cleanup; - 1,000 deterministic exact-position insertions, single and contiguous-range removals, single-row replacements, and exact-window replacements match normal React; compiler tests cover guarded runtime positions plus literal and compiler-safe runtime delete counts, while targeted removal tests require zero surviving key, descriptor, or binding reads, preserve focused input and surrounding DOM identity, and cover native evaluation and coercion, clamping, unsafe runtime positions and counts, custom methods, queued updates, collection-reading rows, StrictMode hydration, and cleanup; targeted window tests additionally require fresh-key work to equal only the incoming window and a 4,096-row same-key refresh to perform zero descriptor creation while preserving all 64 refreshed DOM rows; a 4,096-row mixed window reuses and reorders 48 keyed rows, creates only 16 descriptors, evaluates only the 64 local keys and bindings, and performs the exact 47 local LIS moves plus one fresh-row fragment; a separate 4,096-row sequence grows a 64-row interval to 80 rows and then shrinks it to 40 while preserving both surrounding anchors, reusing every retained row, creating only fresh rows, and performing the exact local LIS moves; another 1,000 randomized variable-length mixed-window updates match normal React; 1,000 queued disjoint grow/shrink updates also match React while targeted tests cover both source orders, adjacent and empty windows, atomic preparation across every window, exact local LIS moves, current delegated event indexes, focus, selection, hydration, Strict Mode, cleanup, and overlapping structural or cross-window key-move fallback; tests also cover duplicate and outside-window key fallback; another 1,000 deterministic queued same-key window refreshes match normal React while disjoint and overlapping targeted tests preserve row identity and perform no descriptor work; 1,000 queued mixed fresh-key and same-key replacements also match React while targeted tests require only the 64 incoming descriptors, preserve every untouched row, and prepare all new rows before mutation; another 1,000 queued overlapping fresh-key replacements match React while targeted tests require work to equal only the final touched union, preserve untouched DOM identity, and cover atomic preparation, existing-key-move fallback, Strict Mode hydration, and unmount cleanup; - 2,000 deterministic batches of two to four reversals match normal React with zero key reads; a 4,096-row direct test requires exactly `n - 1` connected DOM moves, while double-reverse and fallback tests cover zero-move cancellation, custom or unhinted chains, collection-reading rows, StrictMode hydration, and unmount cleanup; validated reorders reuse the runtime-owned row map instead of copying it again, with repeated single and queued reversals checked in both static and hybrid modes; when every row survives, their element lookup stays valid because the same instances hold the current item and index; structural removals still rebuild the lookup and prune removed-row listeners, with delegated clicks checked after successive commits; - 2,000 deterministic batches of two to four randomized sorts match normal React with zero key reads; a 4,096-row direct test requires exactly `n - LIS` connected DOM moves, while mixed sort/reverse chains, native semantics, focus and selection, fallback cases, StrictMode hydration, and cleanup are covered; - the production browser experiment derives a keyed window from 2,048 source rows without rerunning the owner component or corrupting the existing compiler experiments; - the package reactivity benchmark updates one prop across 2,048 bindings, requires identical first/last DOM output, and verifies one compiled render plan against 251 control render plans; - production-size fixtures verify that direct components omit every structural runtime, plain keyed rows omit conditional-row support, and the core-only gzip premium remains at least 50% below the complete compatibility runtime; the checked result is persisted in `packages/farm-react/RUNTIME_SIZE_RESULTS.json`; - the production browser experiment also replaces and reorders interactive items, verifies current keyed DOM identity and capture/stop-propagation behavior, and observes zero owner update executions; - the production browser experiment rotates 1,000 compiler-owned host rows with one LIS move, updates outer and nested row conditions, preserves surviving row and branch identity, removes and inserts a row, and observes zero owner update executions; - the production 10,000/20,000-row benchmark requires nonzero `keyedIdentityTargets`, `keyedMapLookupTargets`, `keyedMembershipTargets`, `keyedCollectionUpdateHints`, and `keyedMapUpdateHints`, `keyedArrayAppendHints`, `keyedArrayPrependHints`, `keyedArrayFilterHints`, `keyedArrayPositionHints`, `keyedArrayReorderHints`, `keyedArraySortHints`, `keyedArrayMappedRollingWindowChainHints`, `keyedArrayRollingWindowHints`, and `keyedArraySliceHints` report counts, preserves the keyed-update speedup floor, checks that scalar selection, Set membership, and Map lookups remain key-directed at scale, compares dense hinted Set/Map updates with equivalent compiled snapshot controls, requires separate single-row and contiguous-range removal speedup floors, requires a mixed local-window reuse/reorder gate to remain at least 4× faster than React and 1.5× faster than its compiled snapshot control, and passes DOM correctness, React-relative regression, direct-delta, and normalized scalability gates; - the public `List` renders iterable and nullish collections correctly with the compiler off; - the packaged runtime, including a keyed-range component root, editable and interactive keyed-row events, row-local conditions, reorders, identity, selection, and hydration, is exercised separately with React 18.3 and React 19; - boolean `data-*` and `aria-*` attributes keep React-compatible string values; and - unsupported list shapes, effects, refs, and unsupported dynamic component-island shapes remain on React without corrupting output. The heavy example measures both a fixed 768-host-node tree with sparse bindings and a component island beside a React-owned 768-node static component. Its compiler-off → compiler-on crossover run measures deliberately favorable supported workloads. An Apple M1 and Chromium 145 reference run measured the direct-binding median at `0.175 ms` without the compiler and `0.015 ms` with it— 91.4% lower, or 11.7× faster—with zero added component executions. The component-island result separately includes the dependent child React render and verifies that the static sibling and compiled owner add zero update executions. Its median child-commit latency fell from `0.165 ms` to `0.025 ms`—84.8% lower, or 6.6× faster. These are warm update-path measurements, not page load, build, layout, paint, network, or general React performance claims. Run the benchmark on target devices before using its timing as a product estimate. The more stable structural result is zero owner and unchanged-sibling executions; direct bindings also avoid a React commit, while a dependent component island still performs its required child commit. ### Recommended rollout 1. Start with `compiler: true` and confirm the application behaves normally. 2. Enable `report` to record coverage across the complete production build. 3. Change `onUnsupported` to `"warn"` to see fallback reasons while editing. 4. Add `"use no compiler"` to known React-owned boundaries when the reason is intentional. 5. Use annotation mode for components whose compiler ownership should be explicit. 6. Use annotation mode with `onUnsupported: "error"` when CI must enforce that selected components remain eligible. The [`examples/react-compiler`](https://github.com/farming-labs/farm.js/tree/main/examples/react-compiler) app contains batching, multiple-binding, common-syntax, calculated-style, controlled-form, primitive-prop, automatic and explicit keyed-list, component-island, compiler-on/off, and heavy-interaction experiments. The standalone starter intentionally keeps the first experience focused. ## React-specific FARMJS APIs Choose React when the application needs the complete built-in client layer: - `Link`, `useRouter`, `useNavigation`, and scroll restoration; - `useAction`, fetcher forms, mutations, and server-query hooks; - `useTheme`, `useLocale`, translations, and the built-in auth hook; - integration providers and generated integration UI; - Markdown/MDX visual routes and the docs adapter; - generated JSX metadata images; - experimental React Server Components and optimized Strata boundaries. Server APIs such as endpoints, server functions, middleware, storage, caching, observability, and deployment use the same contracts described in the renderer overview. ## Production rendering The React adapter uses `renderToPipeableStream` on Node deployment targets and `renderToReadableStream` on edge targets. FARMJS resolves the matching React DOM server entry while building the target and validates that the promised primitive is present. Static generation, ISR, PPR, and ordinary dynamic rendering continue to follow route configuration rather than the component extension. See [Rendering Model](/docs/server-rendering) for rendering modes and [Renderers](/docs/renderers) for the cross-renderer support matrix. --- ## Solid Renderer URL: /docs/renderers/solid Use Solid components, signals, SSR, and hydration with FARMJS routing and renderer-neutral server primitives. # Solid Renderer `@farm.js/solid` connects Solid component compilation, server rendering, and browser hydration to the FARMJS renderer contract. React remains the default unless the application selects Solid. ## Create an app ```bash pnpm create @farm.js/app my-solid-app --template basic --renderer solid --typescript ``` For an existing Basic app, install the adapter and Solid runtime: ```bash pnpm add @farm.js/solid solid-js ``` ```ts import { defineConfig } from "@farm.js/core"; import { solid } from "@farm.js/solid"; export default defineConfig({ renderer: solid(), }); ``` ## Pages and layouts Solid routes use `.tsx` or `.jsx`. Layout children arrive through the normal `children` property: ```tsx import type { LayoutProps, Metadata } from "@farm.js/core"; import "./globals.css"; export const metadata: Metadata = { title: "FARMJS with Solid", }; export default function RootLayout(props: LayoutProps) { return <>{props.children}</>; } ``` Use Solid primitives inside interactive components and mark the component with `"use client"`: ```tsx "use client"; import { createSignal } from "solid-js"; export function Counter() { const [count, setCount] = createSignal(0); return <button onClick={() => setCount((value) => value + 1)}>Count: {count()}</button>; } ``` FARMJS streams the route through Solid's native SSR runtime and hydrates the Solid boundary in the browser. The adapter exposes both Node and WHATWG Web stream modes. ## Call FARMJS server code API routes, endpoint schemas, server functions, middleware, cache, storage, and observability are renderer-neutral. Use the generated API client from Solid UI and keep the privileged handler in a server module: ```tsx "use client"; import { createSignal } from "solid-js"; import { api } from "../../lib/api.generated"; export function Greeting() { const [message, setMessage] = createSignal("Ready"); const callServer = async () => { const result = await api.greeting.post({ body: { name: "Solid" } }); if (result.data) setMessage(result.data.message); }; return <button onClick={callServer}>{message()}</button>; } ``` ## Current boundaries Use the Solid-native bindings for reactive router, action, server-query, theme, and i18n state: ```tsx import { useAction, useRouter, useTheme } from "@farm.js/solid/bindings"; const router = useRouter(); const save = useAction(saveProduct); const theme = useTheme(); await save({ name: "FARMJS" }); await router.push("/products"); theme.toggleTheme(); ``` The binding properties are backed by Solid signals. Renderer-specific `Link` and form components, fetchers, integration providers, programmatic UI routes, Markdown/MDX visual pages, the docs adapter, and generated JSX metadata images remain React-oriented today. The Better Auth starter includes native Solid routes, signals, and forms: ```bash pnpm create @farm.js/app my-auth-app --template better-auth --renderer solid --typescript ``` Other integration starter templates currently target React, so add their renderer-neutral provider code to a native Basic starter. Run the complete example: ```bash pnpm --filter farm-solid-renderer-example dev ``` See the [Solid renderer example](https://github.com/farming-labs/farm.js/tree/main/examples/solid-renderer) and the [renderer support matrix](/docs/renderers). --- ## Svelte Renderer URL: /docs/renderers/svelte Use Svelte 5 components, runes, SSR, hydration, and typed FARMJS server calls through the @farm.js/svelte adapter. # Svelte Renderer `@farm.js/svelte` connects Svelte component compilation, server rendering, and browser hydration to the FARMJS renderer contract. React remains the default unless the application selects Svelte. ## Create an app ```bash pnpm create @farm.js/app my-svelte-app --template basic --renderer svelte --typescript ``` For an existing Basic app, install the adapter and Svelte 5: ```bash pnpm add @farm.js/svelte svelte ``` ```ts import { defineConfig } from "@farm.js/core"; import { svelte } from "@farm.js/svelte"; export default defineConfig({ renderer: svelte(), }); ``` ## Pages and layouts Svelte routes use `.svelte` files: ```text src/app/layout.svelte src/app/page.svelte src/app/products/[id]/page.svelte ``` Put FARMJS route exports in a module script. A layout renders its route content through Svelte's `children` snippet: ```svelte <script module lang="ts"> import type { Metadata } from "@farm.js/core"; import "./globals.css"; export const metadata: Metadata = { title: "FARMJS with Svelte", }; </script> <script lang="ts"> import type { Snippet } from "svelte"; let { children }: { children?: Snippet } = $props(); </script> {@render children?.()} ``` Page route props such as `params` and `searchParams` are available through `$props()` when the component needs them. ## Hydrate interactive routes Export `hydrate = true` from the module script, then use Svelte runes and events normally: ```svelte <script module lang="ts"> export const hydrate = true; </script> <script lang="ts"> let count = $state(0); </script> <button type="button" onclick={() => (count += 1)}>Count: {count}</button> ``` FARMJS server-renders the component with Svelte's server runtime and claims the existing markup with Svelte hydration in the browser. ## Call FARMJS server code API routes, endpoint schemas, server functions, middleware, cache, storage, and observability are renderer-neutral. Use the generated typed API client from a Svelte component: ```svelte <script lang="ts"> import { api } from "../lib/api.generated"; let message = $state("Ready"); async function callServer() { const result = await api.greeting.post({ body: { name: "Svelte" } }); if (result.data) message = result.data.message; } </script> <button type="button" onclick={callServer}>{message}</button> ``` The endpoint can call a validated `createServerFn`; its handler, database access, and secrets remain in the server bundle. ## Current boundaries Use the Svelte-native bindings for router, action, server-query, theme, and i18n stores: ```svelte <script lang="ts"> import { createAction, createRouter, createTheme } from "@farm.js/svelte/bindings"; const router = createRouter(); const save = createAction(saveProduct); const theme = createTheme(); </script> <button on:click={() => save({ name: "FARMJS" })} disabled={$save.pending}>Save</button> ``` The returned values implement Svelte's readable-store contract. Renderer-specific `Link` and form components, fetchers, integration providers, programmatic UI routes, Markdown/MDX visual pages, the docs adapter, and generated JSX metadata images remain React-oriented today. The Better Auth starter includes native Svelte routes, runes, and forms: ```bash pnpm create @farm.js/app my-auth-app --template better-auth --renderer svelte --typescript ``` Other integration starter templates currently target React, so add their renderer-neutral provider code to a native Basic starter. Run the complete example: ```bash pnpm --filter farm-svelte-renderer-example dev ``` See the [Svelte renderer example](https://github.com/farming-labs/farm.js/tree/main/examples/svelte-renderer), the [renderer support matrix](/docs/renderers), and Svelte's [SSR guide](https://svelte.dev/docs/svelte/svelte-server). --- ## Vue Renderer URL: /docs/renderers/vue Use Vue Single-File Components, SSR, hydration, and typed FARMJS server calls through the @farm.js/vue adapter. # Vue Renderer `@farm.js/vue` connects Vue Single-File Component compilation, `createSSRApp` rendering, and browser hydration to the FARMJS renderer contract. React remains the default unless the application selects Vue. ## Create an app ```bash pnpm create @farm.js/app my-vue-app --template basic --renderer vue --typescript ``` For an existing Basic app, install the adapter and Vue runtime: ```bash pnpm add @farm.js/vue vue ``` ```ts import { defineConfig } from "@farm.js/core"; import { vue } from "@farm.js/vue"; export default defineConfig({ renderer: vue(), }); ``` ## Pages and layouts Vue routes use `.vue` files: ```text src/app/layout.vue src/app/page.vue src/app/products/[id]/page.vue ``` Use a normal script for FARMJS route exports and `<script setup>` for the Vue component. A layout renders its children through the default slot: ```vue <script lang="ts"> import type { Metadata } from "@farm.js/core"; import "./globals.css"; export const metadata: Metadata = { title: "FARMJS with Vue", }; </script> <script setup lang="ts"> defineOptions({ inheritAttrs: false }); </script> <template> <slot /> </template> ``` `inheritAttrs: false` prevents FARMJS route props such as `path` from falling through to the root DOM element. Declare the props with `defineProps` when the page needs them. ## Hydrate interactive routes Export `hydrate = true` from the route's normal script, then use Vue state and events normally: ```vue <script lang="ts"> export const hydrate = true; </script> <script setup lang="ts"> import { ref } from "vue"; defineOptions({ inheritAttrs: false }); const count = ref(0); </script> <template> <button type="button" @click="count += 1">Count: {{ count }}</button> </template> ``` FARMJS server-renders the SFC with `createSSRApp` and Vue's native Node or WHATWG Web stream, then uses `createSSRApp` again to claim the existing browser markup. ## Call FARMJS server code API routes, endpoint schemas, server functions, middleware, cache, storage, and observability are renderer-neutral. Use the generated typed API client from the SFC: ```vue <script setup lang="ts"> import { ref } from "vue"; import { api } from "../lib/api.generated"; const message = ref("Ready"); async function callServer() { const result = await api.greeting.post({ body: { name: "Vue" } }); if (result.data) message.value = result.data.message; } </script> <template> <button type="button" @click="callServer">{{ message }}</button> </template> ``` The endpoint can call a validated `createServerFn`; its handler, database access, and secrets remain in the server bundle. ## Install Vue plugins Pass an app setup module to `vue()` to install plugins such as Pinia, vue-i18n, or a component library with `app.use()`: **farm.config.ts** ```ts import { defineConfig } from "@farm.js/core"; import { vue } from "@farm.js/vue"; export default defineConfig({ renderer: vue({ app: "./src/vue-app.ts" }), }); ``` **src/vue-app.ts** ```ts import { defineVueApp } from "@farm.js/vue"; import { createPinia } from "pinia"; export default defineVueApp((app, { ssr }) => { app.use(createPinia()); }); ``` Farm calls the function for every Vue app it creates: once per server render, so plugin state such as a Pinia store is never shared between requests, and once for each hydrated root in the browser. `ssr` is `true` on the server. State created during the server render is not transferred to the browser; stores start from their initial state when the page hydrates. In development, edits to the module apply on the next request. ## Current boundaries Use the Vue-native composables for reactive router, action, server-query, theme, and i18n state: ```ts import { useAction, useRouter, useTheme } from "@farm.js/vue/bindings"; const router = useRouter(); const save = useAction(saveProduct); const theme = useTheme(); await save({ name: "FARMJS" }); await router.push("/products"); theme.toggleTheme(); ``` The composables return Vue refs and computed values. Renderer-specific `Link` and form components, fetchers, integration providers, programmatic UI routes, Markdown/MDX visual pages, the docs adapter, and generated JSX metadata images remain React-oriented today. The Better Auth starter includes native Vue SFC routes, composables, and forms: ```bash pnpm create @farm.js/app my-auth-app --template better-auth --renderer vue --typescript ``` Other integration starter templates currently target React, so add their renderer-neutral provider code to a native Basic starter. Run the complete example: ```bash pnpm --filter farm-vue-renderer-example dev ``` See the [Vue renderer example](https://github.com/farming-labs/farm.js/tree/main/examples/vue-renderer), the [renderer support matrix](/docs/renderers), and Vue's [SSR guide](https://vuejs.org/guide/scaling-up/ssr). --- ## Route Runtime URL: /docs/route-runtime Choose where dynamic pages and API routes execute, inherit deployment defaults from layouts, and set provider duration and region hints. # Route Runtime Choose the execution runtime, regions, and maximum duration for dynamic pages and API routes. Farm uses one contract across file routes, programmatic routes, layouts, and `routeRules`. ## File pages Export the controls next to the page that owns them. No wrapper or additional route file is required. **src/app/reports/page.tsx** ```tsx export const runtime = "node"; export const regions = ["iad1", "fra1"]; export const maxDuration = 30; export default async function ReportsPage() { const reports = await getReports(); return <Reports reports={reports} />; } ``` `maxDuration` is measured in seconds. Region identifiers belong to the selected deployment provider; Farm validates the portable shape and lets the adapter interpret the identifiers. ## API routes API files use the same exports. **src/app/api/reports/route.ts** ```ts export const runtime = "node"; export const regions = ["fra1"]; export const maxDuration = 15; export async function GET() { return Response.json(await getReports()); } ``` Layouts do not wrap API handlers, so API routes inherit matching `routeRules`, then apply their own named exports. ## Layout defaults A layout sets defaults for pages below its segment. The complete layout tree executes in the runtime selected for the final page. **src/app/admin/layout.tsx** ```tsx import type { LayoutProps } from "@farm.js/core"; export const runtime = "node"; export const regions = ["fra1"]; export const maxDuration = 60; export default function AdminLayout({ children }: LayoutProps) { return <main>{children}</main>; } ``` A child can override only the value it needs: **src/app/admin/analytics/page.tsx** ```tsx export const maxDuration = 20; export default function AnalyticsPage() { return <Analytics />; } ``` Farm resolves controls from lowest to highest precedence: 1. Broad matching `routeRules` 2. More-specific matching `routeRules` 3. Parent layouts 4. Nearest layout 5. `page.tsx` or API `route.ts` ## Reset inherited values Use `"auto"` when a route should return to the deployment provider's default instead of inheriting a layout or rule. ```tsx export const runtime = "auto"; export const regions = "auto"; export const maxDuration = "auto"; ``` An omitted export inherits. An explicit `"auto"` resets. ## Programmatic routes The same fields are typed on every programmatic primitive. **src/farm.routes.tsx** ```tsx import { defineRoutes } from "@farm.js/core"; import { ReportsPage } from "./features/reports/page"; export default defineRoutes(({ page, layout, api }) => [ page("/reports", { runtime: "node", regions: ["iad1", "fra1"], maxDuration: 30, component: ReportsPage, }), layout("/admin", { runtime: "node", maxDuration: 60, component: AdminLayout, }), api("/api/reports", { runtime: "node", regions: ["fra1"], maxDuration: 15, GET: async () => Response.json(await getReports()), }), ]); ``` ## Route rules Use route rules when deployment policy belongs to a URL group rather than one source file. **farm.config.ts** ```ts import { defineConfig } from "@farm.js/core"; export default defineConfig({ routeRules: { "/api/public/**": { runtime: "node", regions: ["iad1"], maxDuration: 10, }, "/admin/**": { runtime: "node", regions: ["fra1"], maxDuration: 60, }, }, }); ``` Page, API, and layout values override matching rules. Rules still apply to generated integration endpoints that do not have a page file. ## Deployment behavior Farm writes the normalized result to `.farm/route-runtime-manifest.json` on production builds. Custom deployment adapters can consume this manifest instead of parsing source files. | Preset family | Runtime accepted | `regions` | `maxDuration` | | ------------------ | ---------------- | ------------------------------- | ------------------------------- | | `vercel` | `auto`, `node` | Enforced per generated function | Enforced per generated function | | Cloudflare presets | `auto`, `edge` | Preserved for adapters | Preserved for adapters | | `netlify` | `auto`, `node` | Preserved for adapters | Preserved for adapters | | `netlify-edge` | `auto`, `edge` | Preserved for adapters | Preserved for adapters | | `node-server` | `auto`, `node` | Preserved in the manifest | Preserved in the manifest | | Custom preset | Adapter-defined | Preserved in the manifest | Preserved in the manifest | For Vercel Node builds, Farm creates one Build Output API function per distinct region/duration policy and routes matching requests to it. Routes using provider defaults continue using the base function. A production build fails when a dynamic route requests an incompatible runtime, such as `runtime = "edge"` with the Node-based `vercel` preset or `runtime = "node"` with a Cloudflare Worker preset. Farm warns when a valid runtime target cannot map a region or duration hint, while keeping the value available in the manifest. Farm's current Vercel adapter emits Node functions. Use a supported Edge preset such as `cloudflare-module` or `netlify-edge` for `runtime = "edge"`; do not treat a Vercel region identifier as a portable Edge guarantee. ## Static routes and development Static pages are generated during the build and do not have a request-time function. Runtime, region, and duration controls therefore do not change how a fully static page is served. The local development server runs in Node so one process can provide HMR and route discovery. Runtime compatibility and provider output are enforced by `farm build`, which is the command CI should run before deployment. ## Practical guidance - Keep functions close to their database, not necessarily close to every browser. - Set `maxDuration` high enough for normal streaming responses but low enough to cap runaway work. - Use `edge` only when the complete page, layout tree, middleware, and dependencies support Web APIs without Node-only modules. - Prefer layout defaults for coherent application areas and route rules for generated or cross-cutting endpoints. - Avoid configuring static pages unless the same policy also applies when the page becomes dynamic later. --- ## Routing URL: /docs/routing Farm uses an app directory routing model with static routes, dynamic segments, catch-all routes, and typed navigation. # Routing Farm uses an app directory routing model with static routes, dynamic segments, catch-all routes, and typed navigation. The examples on this page use the default React renderer. Solid uses `.tsx`/`.jsx` route files, Vue uses `.vue`, and Svelte uses `.svelte`. The route tree and server contracts stay the same; see [Renderers](/docs/renderers) for component conventions and feature compatibility. ## File routes | File | URL | | ------------------------------- | ------------- | | src/app/page.tsx | / | | src/app/about/page.tsx | /about | | src/app/about/page.mdx | /about | | src/app/blog/[slug]/page.tsx | /blog/:slug | | src/app/docs/[...slug]/page.tsx | /docs/:slug\* | With Vue, the same routes use names such as `src/app/page.vue` and `src/app/blog/[slug]/page.vue`. With Svelte, use `src/app/page.svelte` and `src/app/blog/[slug]/page.svelte`. ## Programmatic routes File routes cover most apps. Define routes in code instead when a URL can't be a folder, or when a page and the endpoints it needs belong together. Export `defineRoutes` from `src/routes.ts` (or `routes.tsx`, `farm.routes.ts`, `farm.routes.tsx`, and their `.js`/`.jsx` forms): **src/routes.tsx** ```tsx import { createEndpoint, defineRoutes } from "@farm.js/core"; import { z } from "zod"; import { ChangelogPage } from "./features/changelog/page"; import { listReleases, renderReleasesFeed } from "./features/changelog/releases"; export default defineRoutes(({ page, api }) => [ page("/changelog", { component: ChangelogPage }), api("/changelog/feed.xml", { GET: async () => new Response(renderReleasesFeed(await listReleases()), { headers: { "content-type": "application/rss+xml" }, }), }), api("/changelog/releases.json", { GET: createEndpoint( { method: "GET", query: z.object({ limit: z.coerce.number().int().positive().default(20) }), }, async ({ query }) => Response.json(await listReleases(query.limit)), ), }), ]); ``` - `api()` takes the same handlers a `route.ts` exports, one per method (`GET`, `HEAD`, `QUERY`, `POST`, `PUT`, `DELETE`, `PATCH`, `OPTIONS`): a plain `(request) => Response` or a `createEndpoint(...)` with request validation. - Paths don't need the `/api` prefix that file API routes live under, and they can end in a file extension such as `/feed.xml` or `/data.json`. - `page()` and `layout()` take a component plus the route options covered below: data, guards, search params, pending, error and not-found UI, and [route runtime](/docs/route-runtime) settings. - Programmatic pages join the same route table as file routes, so generated route types, `PageProps` and typed links include them. Programmatic API routes are not in the generated API client types yet; use a file route under `src/app/api` when you need a typed client. Prefer a file route when the URL maps to a folder. Reach for `defineRoutes` for colocated features, URLs a folder can't express, and route lists generated from data or configuration. ## Named slots and intercepted routes An `@name` directory gives its owning layout another rendered node alongside `children`. Use slots for independently composed areas such as activity panels, drawers, and modals. The slot name and interception marker never become part of the public URL. ```text src/app/feed/ ├── layout.tsx ├── page.tsx ├── photo/[id]/page.tsx └── @modal/ ├── default.tsx └── (.)photo/[id]/page.tsx ``` The layout receives the `@modal` result as `modal`: ```tsx import type { ReactNode } from "react"; export default function FeedLayout({ children, modal, }: { children: ReactNode; modal?: ReactNode; }) { return ( <main> {children} {modal} </main> ); } ``` `default.tsx` renders when no page in that slot matches. A client-side navigation from `/feed` to `/feed/photo/42` selects `@modal/(.)photo/[id]/page.tsx`, places it in the existing modal slot, and keeps the surrounding feed state alive. A direct request or refresh at `/feed/photo/42` renders the canonical `photo/[id]/page.tsx` instead. Interception markers are relative to the slot's owning route: | Marker | Target base | | ---------- | ---------------------- | | `(.)` | Same route level | | `(..)` | One route level above | | `(..)(..)` | Two route levels above | | `(...)` | App root | Make an intercepting slot a Client Component when it needs event handlers such as closing a modal with `router.back()`. Farm falls back to canonical document navigation if it cannot safely mount the intercepted slot in the current page. ## Dynamic params **src/app/users/[id]/page.tsx** ```tsx import type { PageProps } from "@farm.js/core"; export default function UserPage({ params }: PageProps<"/users/[id]">) { return <div>User: {params.id}</div>; } ``` The route literal is checked against the generated application routes and narrows `params` to `{ id: string }`. The same route-aware generic is available on `LayoutProps`, `LoadingProps`, `ErrorProps`, `MetadataProps`, and `LayoutMetadataProps`. Omitting the generic keeps the existing `Record<string, string>` behavior for gradual adoption. Use `GenerateStaticParams` to check build-time paths against the same route: ```tsx import type { GenerateStaticParams } from "@farm.js/core"; export const generateStaticParams: GenerateStaticParams<"/users/[id]"> = async () => [ { id: "ada" }, { id: "grace" }, ]; ``` For an optional catch-all such as `/docs/[[...slug]]`, omit `slug` or return `slug: []` to generate the parent `/docs` page. Farm exposes that empty capture as `params.slug === ""`, the same value produced by request-time routing. JavaScript callers that return `{ slug: undefined }` or `{ slug: null }` are normalized to the same empty value; typed callers should prefer omission or an empty array. ## Typed navigation Farm writes the route union into the consolidated `src/farm.d.ts` declaration file. Link hrefs and route component props accept real routes without widening everything to plain string. Link hrefs can also include query strings and hash fragments. Changing only the fragment preserves SPA state, honors push versus replace history, and does not request route data again. Relative navigation, prefetch, and shallow history URLs resolve against the current document in development and production. For example, `?tab=settings` from `/users/123?tab=profile` keeps `/users/123`, and `456` from `/users/123` resolves to `/users/456`. Native anchor behavior still takes precedence: for example, a `Link` with a `download` attribute is handled by the browser instead of Farm's SPA router. Absolute URI schemes such as `mailto:`, `tel:`, `sms:`, and same-origin `blob:` URLs are passed through unchanged and are never prefetched as app routes. Literal custom schemes such as `customapp:open` are validated from their URI grammar and work without registration. Viewport prefetch uses a short scroll guard and is cancelled if its link unmounts before the guard expires. Intent prefetches are deduplicated while active; after an attempt settles, a later hover, focus, or touch can retry while successful route data remains deduplicated by the router cache. The development page-data router reuses a successfully decoded JSON prefetch response when navigation overlaps it with the same URL, interception origin, and active layout chain. Refreshes and different contexts fetch independently, and failed prefetches are retried by navigation. Cancelling a waiting navigation does not cancel its background prefetch. Deferred streaming responses keep independent requests and cancellation ownership; a waiting navigation starts its own request once the prefetch response headers identify a deferred stream. Production HTML navigation also reuses matching in-flight prefetches. Different interception contexts and deployment identities stay separate; failed prefetches are retried by navigation. Refresh bypasses pending prefetches, and clearing the cache prevents their late responses from repopulating it. Cancelling navigation does not abort a shared background prefetch. When a production HTML request redirects, Farm hands the original URL to a full document navigation so the browser preserves the redirect destination, fragments, and push/replace history. Prefetching a redirect never navigates the page or caches destination HTML under the original URL. A same-origin link whose response is not HTML, such as an RSS feed, JSON, or a file served by an API route, is handed to the browser the same way, so it opens as its own document. Prefetching such a link never caches it as page HTML. Internal `Link` hrefs stay app-relative: when `basePath: "/console"` is configured, `href="/about"` renders and navigates to `/console/about`. Do not add the base path to route hrefs yourself. For a reusable custom-scheme type, use ``ExternalHref<`customapp:${string}`>`` (or declaration-merge `LinkExternalUriSchemes` when the scheme should belong to the default `ExternalHref` union). **Client navigation** ```tsx import { Link } from "@farm.js/core/client"; export function Nav() { return ( <> <Link href="/about">About</Link> <Link href="/blog/farm-routing?from=docs">Routing</Link> </> ); } ``` Same-page fragment links (such as `#settings`) and Back/Forward between those fragments keep the current page mounted without loading route data. Native anchor scrolling and `hashchange` listeners still work. A different pathname, query string, or intercepted-route context still performs route navigation. ## Lightweight router helpers Use the lightweight router when client components, layouts, breadcrumbs, tabs, or tests need to match app routes without adding a separate routing library. **src/lib/router.ts** ```ts import { createFarmRouter } from "@farm.js/core/router"; export const router = createFarmRouter(["/", "/dashboard", "/users/[id]", "/docs/[[...slug]]"]); ``` ```ts const match = router.match("/users/ada?tab=settings"); if (match) { console.log(match.route.path); // /users/[id] console.log(match.params.id); // ada } ``` Build hrefs from the same route patterns: ```ts const href = router.build("/docs/[[...slug]]", { slug: ["core", "routing"], }); ``` This returns `/docs/core/routing`. Optional catch-all params can be omitted, static routes win over dynamic routes, and route groups such as `(marketing)` do not appear in the URL. Programmatic route patterns must remain stable when a browser parses their URLs. Farm rejects dot-only segments, encoded path separators or control characters, and raw backslashes instead of building a URL that the browser would canonicalize to a different route. Ordinary encoded static segments such as `/files/hello%20farm` remain supported. These matching rules also apply with experimental React Server Components enabled. Farm prepares page precedence when the route entry initializes, so filesystem discovery order cannot make `/users/[id]` hide `/users/new`. Optional catch-all pages match both their parent URL and deeper paths. Route matching decodes each URL path segment once before comparing static names or exposing `params`. Route names remain literal (including `%`), and malformed percent escapes remain literal instead of aborting the request. A required or optional catch-all must be the final URL segment. Farm reports paths such as `docs/[...slug]/edit/page.tsx` during route discovery because the catch-all would otherwise consume the `edit` segment and make the route unreachable. Each dynamic segment in one route must have a unique parameter name. Farm rejects paths such as `teams/[id]/members/[id]/page.tsx` instead of silently replacing the outer `id` value. The prototype-sensitive names `__proto__`, `constructor`, and `prototype` are reserved because JavaScript cannot represent them safely in the plain `params` object. For navigation state, `router.isActive(pattern, pathname, { exact: false })` also matches descendants after dynamic segments, such as `/users/42/settings` for `/users/[id]`. Supplied path parameters must contain non-empty segments; omit an optional catch-all instead of passing an empty string. Client components can pass the same route list to `useRouter` when they want current route params: ```tsx import { useRouter } from "@farm.js/core/client"; export function CurrentUserTab() { const router = useRouter({ routes: ["/users/[id]", "/users/[id]/settings"], }); return <span>{router.params.id}</span>; } ``` Imperative `push()` and `replace()` calls use SPA navigation for same-origin routes. Absolute cross-origin URLs use normal document navigation instead of Farm's local page-data endpoint. Internal router paths use the configured application `basePath`, matching `Link`; already-prefixed paths are preserved. ## Navigation blocking Use `useBlocker` when a client component needs to protect unsaved work before SPA navigation continues. ```tsx "use client"; import { useBlocker } from "@farm.js/core/client"; export function ProductForm({ isDirty }: { isDirty: boolean }) { useBlocker({ when: isDirty, message: "You have unsaved changes.", }); return <form>{/* ... */}</form>; } ``` Blockers apply to Farm SPA navigation and browser unload prompts. They improve UX, but they do not replace server-side validation or persistence checks. ## Page state Use page state for shallow UI state that belongs in browser history but should not reload route data: modals, drawers, selected panels, or temporary filters. ```tsx "use client"; import { usePageState, useRouter } from "@farm.js/core/client"; export function ProductToolbar() { const router = useRouter(); const page = usePageState<{ modal?: "cart"; drawer?: "filters" }>(); return ( <> <button onClick={() => router.pushState({ modal: "cart" })}>Cart</button> <button onClick={() => router.replaceState({ drawer: "filters" })}>Filters</button> {page?.modal === "cart" ? <CartModal /> : null} </> ); } ``` Page state is stored in `history.state`, so back/forward navigation restores the previous state without changing the URL unless you pass an href. ## Scroll restoration Farm restores window scroll during SPA navigation. Register nested scroll areas when a layout owns its own scroll container. Locations with different query strings keep independent positions, so browser history restores the correct filtered, searched, or paginated view. ```tsx "use client"; import { useScrollRestoration } from "@farm.js/core/client"; export function DocsSidebar() { const ref = useScrollRestoration<HTMLDivElement>("docs-sidebar"); return <div ref={ref}>{/* links */}</div>; } ``` Use stable keys per scroll container. If two elements share a key, the latest mounted element owns that stored position. ## Route data cache Programmatic routes can cache the value returned from `data.main`. This is useful for product pages, docs pages, dashboards, and other route data that should be reused during server rendering or prefetching. The first `createRoute` argument is a pathname pattern only; declare typed search parameters with `search` and add query strings or hashes when building a navigation URL. ```tsx import { createRoute, invalidate } from "@farm.js/core"; import { z } from "zod"; import { ProductPage } from "./page"; export const ProductRoute = createRoute("/products/[id]", { params: z.object({ id: z.string() }), data: { key: ({ params }) => ["product", params.id], staleTime: "30s", async main({ params }) { return { product: await db.product.findUnique({ where: { id: params.id } }), }; }, }, component: ProductPage, }); export async function saveProduct(id: string, name: string) { await db.product.update({ where: { id }, data: { name } }); await invalidate(["product", id]); } ``` `key` enables caching. When a cached entry is still fresh, Farm reuses the previous `data.main` result. `before` still runs for each request, and `after` still runs with the returned data, so setup and logging hooks keep their normal behavior. `staleTime` accepts a number of milliseconds or a duration string such as `"500ms"`, `"30s"`, `"5m"`, or `"1h"`. Omit `staleTime` when data should stay cached until invalidated. Farm also tags route data by the rendered path, so `revalidatePath("/products/123")` invalidates the matching route data entry. Use `tags` or `paths` when one mutation should refresh more than one route: ```tsx export const ProductRoute = createRoute("/products/[id]", { data: { key: ({ params }) => ["product", params.id], tags: ({ params }) => [`product:${params.id}`, "products"], paths: ({ params }) => [`/products/${params.id}`, "/products"], async main({ params }) { return { product: await getProduct(params.id) }; }, }, component: ProductPage, }); ``` Cache keys are part of your data security model. If data depends on the current user, role, tenant, locale, or draft mode, include that value in `key` or avoid caching that route. Route cache invalidation improves freshness, but API routes and server functions still need their own authorization checks. ## Route actions An action is a typed server function, not necessarily a URL route. Use a standalone `createServerFn` when an operation is shared by several pages, belongs to a background job or integration, or does not have one resource owner: ```ts import { createServerFn } from "@farm.js/core/server-fn"; import { z } from "zod"; export const sendInvite = createServerFn({ input: z.object({ email: z.string().email() }), async handler({ input }) { return { queued: true, email: input.email }; }, }); ``` ```tsx import { useAction } from "@farm.js/core/client"; import { sendInvite as sendInviteFn } from "./actions"; const sendInvite = useAction(sendInviteFn); await sendInvite({ email: "ada@example.com" }); ``` Attach actions to a route when they are commands on that route's resource. This gives the page a typed namespace for related mutations, lets `useAction(route)` select a declared default, and lets Farm expose an action-only client proxy without shipping the route loader, component, or database code to the browser. The action still uses Farm's server-function transport; it does not become a public URL such as `/products/update`. With React Server Components and Server Actions enabled, a programmatic route can own named server functions. Keep the functions in a dedicated server module so Farm can replace the route with an action-only proxy when a Client Component imports it. The proxy contains no loader, component, or database code. **src/features/products/actions.ts** ```ts import { createServerFn } from "@farm.js/core/server-fn"; import { z } from "zod"; import { db } from "./db"; export const updateProduct = createServerFn({ input: z.object({ id: z.string(), name: z.string().min(2), }), invalidates: ({ input }) => [{ key: ["product", input.id] }], async handler({ input }) { return db.product.update({ where: { id: input.id }, data: { name: input.name }, }); }, }); export const publishProduct = createServerFn({ input: z.object({ id: z.string() }), async handler({ input }) { return db.product.update({ where: { id: input.id }, data: { published: true }, }); }, }); ``` Attach the imported functions to the route. `defaultAction` selects the function returned by `route.action` and used by `useAction(route)`. When the route has one action, or no explicit default, Farm selects the first declared action. **src/features/products/product.route.tsx** ```tsx import { createRoute } from "@farm.js/core/routes"; import { ProductPage } from "./product-page"; import { publishProduct, updateProduct } from "./actions"; import { db } from "./db"; export const ProductRoute = createRoute("/products/[id]", { data: { key: ({ params }) => ["product", params.id], async main({ params }) { return { product: await db.product.findUniqueOrThrow({ where: { id: params.id } }) }; }, }, actions: { update: updateProduct, publish: publishProduct, }, defaultAction: "update", component: ProductPage, }); ``` Server code calls either the default or a named action as a normal typed function. Direct calls run in process, while input/output validation, middleware, declared errors, and invalidation keep their server-function behavior. ```ts await ProductRoute.action({ id: "p1", name: "Keyboard" }); await ProductRoute.actions.publish({ id: "p1" }); ``` In a Client Component, `useAction(ProductRoute)` wraps the default function in a callable RPC and adds React state. Call the wrapper itself; no `.submit()` method is required. ```tsx "use client"; import { useAction } from "@farm.js/core/client"; import { ProductRoute } from "./product.route"; export function RenameProduct({ id }: { id: string }) { const update = useAction(ProductRoute); return ( <button disabled={update.pending} onClick={() => update({ id, name: "Keyboard" })}> {update.pending ? "Saving…" : "Rename"} </button> ); } ``` The wrapper exposes `pending`, `status`, `data`, `error`, `reset`, `formAction`, and `Form`. It keeps the action's input, result, and declared error types. Choose another named action explicitly when needed: ```tsx const publish = useAction(ProductRoute.actions.publish); const result = await publish({ id: "p1" }); ``` For both `useAction` and `useServerFn`, `pending` means at least one submission since the last reset is still running. `status`, `result` / `data`, and `error` describe the latest submission. If that submission finishes before an older one, its status becomes `success` or `error` while `pending` remains `true`. Finishing older work only reduces the pending count; it cannot replace the latest result, error, or status. A new submission sets status back to `pending`, and `reset()` returns it to `idle`. The same wrapper supports progressive forms. The form performs a native server action before hydration and uses the tracked RPC lifecycle after hydration: ```tsx const update = useAction(ProductRoute); return ( <update.Form> <input type="hidden" name="id" value={product.id} /> <input name="name" defaultValue={product.name} /> <button disabled={update.pending}>Save</button> </update.Form> ); ``` Route action entries must be imported identifiers such as `{ update }` or `{ update: updateProduct }`. Do not create them inline inside `createRoute`; the separate module is the server boundary Farm uses to generate safe browser references. When provided, `defaultAction` must be a string literal matching one of those entries. `useAction` keeps `Form`'s component identity stable across renders, including when you pass inline `optimistic` callbacks or change the target action. Existing fields retain their DOM identity, unsaved input, focus, and selection. Submissions use the latest action and options; React's normal form reset after a successful function action still applies. ## Deferred route data Use `defer()` for secondary data that should not delay the route shell. Farm returns the value from `data.main` as soon as its directly awaited work finishes, then streams explicitly deferred fields into nested React Suspense boundaries. **src/features/products/page.tsx** ```tsx import { Suspense, use } from "react"; import type { Deferred } from "@farm.js/core"; export function ProductPage({ data }: ProductPageProps) { return ( <main> <h1>{data.product.name}</h1> <Suspense fallback={<ReviewsSkeleton />}> <Reviews reviews={data.reviews} /> </Suspense> </main> ); } function Reviews({ reviews }: { reviews: Deferred<Review[]> }) { const resolvedReviews = use(reviews); return resolvedReviews.map((review) => <ReviewRow key={review.id} review={review} />); } ``` **src/farm.routes.tsx** ```tsx import { createRoute, defer } from "@farm.js/core"; import { ProductPage } from "./features/products/page"; export const ProductRoute = createRoute("/products/[id]", { data: { async main({ params }) { const reviews = defer(getProductReviews(params.id)); const product = await getProduct(params.id); return { product, reviews, }; }, }, component: ProductPage, }); ``` In this example, both requests start together, but only `getProduct` controls when the route shell is ready. `getProductReviews` does not block the shell. The component receives `reviews` as `Deferred<Review[]>`, so React `use()` resolves it with full type inference. `data.after` runs after `main` returns and receives the deferred promise without waiting for it. This keeps logging and request cleanup hooks from extending the stream; await a deferred field inside `after` only when that delay is intentional. Farm uses a streaming page-data response for SPA navigation and serializes settled deferred values for hydration. Rejections expose a generic `DeferredDataError` to the browser while the original error remains in server logs. Deferred values and their resolved results must be JSON-serializable when they cross into a hydrated component. Use `defer()` for independent secondary sections such as reviews, recommendations, activity, or analytics. Keep data required for the title, authorization decision, redirect, or primary above-the-fold content directly awaited in `main`. `defer()` is explicit: ordinary nested promises are not automatically treated as streamed route data. ## Typed Search Params Programmatic routes can validate URL search params and define cleanup rules in one place. Use `search.schema` for typed route input, `stripDefaults` for clean URLs, `preserve` for params that should carry across links, and `temporary` for one-time UI params. ```tsx import { createRoute } from "@farm.js/core"; import { z } from "zod"; export const ProductRoute = createRoute("/products/[id]", { search: { schema: z.object({ tab: z.enum(["info", "reviews"]).default("info"), locale: z.string().default("en"), toast: z.string().optional(), }), stripDefaults: true, preserve: ["locale"], temporary: ["toast"], }, data: { async main({ params, search }) { return { product: await getProduct(params.id), tab: search.tab, }; }, }, component: ProductPage, }); ``` With that route, `/products/123?tab=info&locale=am&toast=saved` gives the component typed search data: ```ts { tab: "info", locale: "am", toast: "saved", } ``` After the route has consumed it, Farm can clean the URL to `/products/123?locale=am`. `tab=info` is removed because it matches the schema default, and `toast=saved` is removed because it is temporary. `preserve` is used by Farm links. If the current page is `/products?locale=am`, then a link to `/products/[id]` carries `locale=am` unless the link already provides its own `locale`. Use this for tab state, pagination defaults, locale/tenant preservation, preview mode, and one-time params such as `toast=saved`. Keep security-sensitive values out of search params; they are user-editable URL state, not trusted server state. ## Route context Use `context` in `farm.config.ts` for request-scoped dependencies that guards and route data need: sessions, tenants, feature flags, or database clients. The value is available to programmatic route `guard`, `data.before`, `data.main`, cache key functions, and `data.after`. ```ts import { defineConfig } from "@farm.js/core"; import { db } from "./src/db"; import { getSession } from "./src/session"; export default defineConfig({ context: async ({ request }) => ({ session: await getSession(request), db, }), }); ``` Add a module augmentation when you want autocomplete in route files: ```ts import type { db } from "./src/db"; import type { getSession } from "./src/session"; declare module "@farm.js/core" { interface FarmAppContext { session: Awaited<ReturnType<typeof getSession>>; db: typeof db; } } ``` Route context is server-only. Farm passes it to guards and data hooks without serializing it into browser props, so keep raw database clients and secrets in `context` and return only safe page data from `data.main`. ## Route guards Use `guard` when a route should be allowed or blocked before route data loads. Guards run after params/search validation and before `data.before` or `data.main`. ```tsx import { createRoute, redirect } from "@farm.js/core"; export const DashboardRoute = createRoute("/dashboard", { guard: async ({ context }) => { if (!context.session.user) { redirect("/login"); } }, data: { async main({ context }) { return { stats: await getDashboardStats(context.db) }; }, }, component: DashboardPage, }); ``` Use `guard` for route flow: auth redirects, role gates, tenant checks, and early `notFound()` decisions. Use `data.before` when you want to prepare values that `data.main` needs. A guard is not a complete authorization boundary; repeat sensitive authorization inside API routes and server functions because those can be requested directly. ## Route UI states Programmatic routes can define local `pending`, `error`, and `notFound` components. `pending` is used as the Suspense fallback while route data resolves. `error` handles guard/data errors. `notFound` handles `notFound()` thrown from guard or data hooks. ```tsx import { notFound } from "@farm.js/core"; export const ProductRoute = createRoute("/products/[id]", { data: { async main({ params }) { const product = await getProduct(params.id); if (!product) notFound(); return { product }; }, }, pending: ProductSkeleton, error: ProductError, notFound: ProductNotFound, component: ProductPage, }); ``` Redirects are not rendered through `error`; they escape so Farm can return a real redirect response. ## Nested segments Folders become URL segments. Use normal folders for visible path segments and dynamic folders when the value comes from the URL. **Route tree** ```txt src/app/ page.tsx dashboard/ page.tsx settings/ page.tsx blog/ [slug]/ page.tsx docs/ [...slug]/ page.tsx ``` This creates `/`, `/dashboard`, `/dashboard/settings`, `/blog/:slug`, and `/docs/:slug*`. ## Markdown pages Use `page.md` or `page.mdx` for static content routes. They behave like app pages, participate in layouts, and get route types. Markdown/MDX visual routes currently use the React content renderer. Preact treats this as a `preact/compat` surface, while Solid, Vue, and Svelte applications can still serve renderer-neutral API content or ordinary static assets but should use renderer-owned component pages for visual routes. **src/app/about/page.mdx** ```mdx # About This page renders at `/about` and exposes source at `/about.md`. ``` When `page.tsx` and `page.md` or `page.mdx` share a folder, the React file owns the HTML route and the markdown file becomes its exact `.md` representation. Without a sidecar, Farm derives markdown from the rendered React page automatically. ## Metadata And OG Images Export `metadata` for static head tags or `generateMetadata` when the values depend on route params, search params, middleware data, or route data. Farm merges layout metadata from root to leaf, then applies the page metadata last. During HTML-based client navigation, Farm reconciles the destination document's title, meta tags, canonical and alternate links, icons, manifest link, and [page JSON-LD](/docs/configuration#agent-readiness). Tags omitted by the destination are removed, so metadata from the previous route cannot remain active. A layout can define a default title and a `%s` template for child segments. The layout itself uses the default; a child string title is substituted into the nearest parent template: ```tsx export const metadata = { title: { default: "Acme", template: "%s | Acme", }, }; ``` **src/app/products/[id]/page.tsx** ```tsx import type { MetadataProps } from "@farm.js/core"; export const metadata = { description: "Product details", openGraph: { siteName: "Acme", type: "website", }, }; export async function generateMetadata({ params }: MetadataProps<"/products/[id]">) { const product = await getProduct(params.id); return { title: product.name, openGraph: { title: product.name, description: product.summary, }, twitter: { card: "summary_large_image", title: product.name, }, }; } export default function ProductPage() { return <main>Product</main>; } ``` Use `generateMetadata` in a layout when the same dynamic metadata flow should apply to every page in a route subtree. Farm passes the matched route params to each layout and merges the result before applying the page metadata. A catch-all docs layout can therefore load the current document for every nested docs URL: **src/app/docs/[...slug]/layout.tsx** ```tsx import type { LayoutProps } from "@farm.js/core"; export async function generateMetadata({ params }: Pick<LayoutProps, "params">) { const slug = params.slug?.split("/") ?? []; const document = await getDocument(slug); return { title: document.title, description: document.description, openGraph: { title: document.title, description: document.description, type: "article", }, twitter: { card: "summary_large_image", title: document.title, description: document.description, }, }; } export default function DocsLayout({ children }: LayoutProps) { return children; } ``` Pair this layout with `opengraph-image.tsx` in the same `[...slug]` segment to generate a different PNG for each document. `generateMetadata` supplies the title, description, and social fields; the image file renders the PNG described below. Leave `openGraph.images` and `twitter.images` unset when Farm should attach the nearest generated image automatically. An explicit image value still takes precedence. ### Article fields When `openGraph.type` is `"article"`, Farm also renders the Open Graph article properties: ```tsx import type { MetadataProps } from "@farm.js/core"; export async function generateMetadata({ params }: MetadataProps<"/blog/[slug]">) { const post = await getPost(params.slug); return { openGraph: { type: "article", publishedTime: post.publishedAt, modifiedTime: post.updatedAt, authors: ["https://acme.test/team/ada"], section: "Engineering", tags: post.tags, }, }; } ``` `publishedTime` and `modifiedTime` take an ISO string or a `Date` and render `article:published_time` and `article:modified_time`. `authors` and `tags` take a string or an array and render one `article:author` or `article:tag` per item. `section` renders `article:section`. Other `openGraph.type` values ignore these fields. ### Favicons Place favicon files in `public/`, then declare them through the root layout metadata. Files in `public/` are served from the application root, so `public/favicon.svg` is available at `/favicon.svg`. **src/app/layout.tsx** ```tsx import type { Metadata } from "@farm.js/core"; export const metadata: Metadata = { icons: "/favicon.svg", }; ``` Use the object form when you need multiple browser icons or an Apple touch icon: ```tsx import type { Metadata } from "@farm.js/core"; export const metadata: Metadata = { icons: { icon: [{ url: "/favicon.svg", type: "image/svg+xml", sizes: "any" }], shortcut: "/favicon.ico", apple: [{ url: "/apple-touch-icon.png", type: "image/png", sizes: "180x180" }], }, }; ``` Root layout metadata applies the favicon to every route. Nested layouts and pages can override individual icon entries through their own metadata. Do not render a `<link rel="icon">` element from the layout component; declaring `metadata.icons` lets Farm place the tags in the document head in both development and production. ### Site verification Search consoles confirm site ownership with a meta tag. Set the tokens once in the root layout: **src/app/layout.tsx** ```tsx import type { Metadata } from "@farm.js/core"; export const metadata: Metadata = { verification: { google: "google-token", bing: "bing-token", yandex: "yandex-token", other: { "facebook-domain-verification": "facebook-token" }, }, }; ``` `google`, `bing`, and `yandex` render `google-site-verification`, `msvalidate.01`, and `yandex-verification`. `other` renders one `<meta name content>` per key. Every field accepts an array when a service needs more than one token. Like `openGraph`, `verification` merges one level deep, so a nested layout or page adds tokens and replaces the ones it repeats. ### Application metadata routes Use server-only metadata files when crawlers or browsers need an application-level document rather than an HTML `<meta>` tag. Farm discovers five conventions in `src/app` and route segments: | File | Public route | Default return type | | -------------- | ----------------------- | ------------------------ | | `sitemap.ts` | `/sitemap.xml` | `MetadataRoute.Sitemap` | | `robots.ts` | `/robots.txt` | `MetadataRoute.Robots` | | `manifest.ts` | `/manifest.webmanifest` | `MetadataRoute.Manifest` | | `llms.ts` | `/llms.txt` | `MetadataRoute.LlmsTxt` | | `llms-full.ts` | `/llms-full.txt` | `MetadataRoute.LlmsTxt` | The default export can be a literal value or a sync or async function. Functions receive the matched `params`, the current `Request`, its `URLSearchParams`, and the concrete route-segment `path`. **src/app/sitemap.ts** ```ts import type { MetadataRoute } from "@farm.js/core"; export const revalidate = 3600; export default async function sitemap(): Promise<MetadataRoute.Sitemap> { const products = await listProducts(); return [ { url: "https://acme.test", lastModified: new Date(), changeFrequency: "daily", priority: 1, }, ...products.map((product) => ({ url: `https://acme.test/products/${product.id}`, lastModified: product.updatedAt, priority: 0.8, })), ]; } ``` Farm escapes XML values and supports language alternates through `alternates.languages`. **src/app/robots.ts** ```ts import type { MetadataRoute } from "@farm.js/core"; export default function robots(): MetadataRoute.Robots { return { rules: { userAgent: "*", allow: "/", disallow: ["/admin/", "/preview/"], }, sitemap: "https://acme.test/sitemap.xml", host: "https://acme.test", }; } ``` `robots.txt` controls crawling. Per-page indexing directives belong in `metadata.robots`, which renders a `<meta name="robots">` tag. Besides `index` and `follow`, it accepts `noarchive`, `nosnippet`, `noimageindex`, `max-snippet`, `max-image-preview`, `max-video-preview`, and `unavailable_after`. `googleBot` takes the same directives, or a string, for a separate `<meta name="googlebot">` tag: ```tsx import type { Metadata } from "@farm.js/core"; export const metadata: Metadata = { robots: { index: true, follow: true, "max-snippet": -1, "max-image-preview": "large", googleBot: { noimageindex: true }, }, }; ``` A root `robots.ts` replaces the robots.txt that [`agent.crawlers`](/docs/configuration#ai-crawlers) generates, and a `public/robots.txt` wins over both. All three take `/robots.txt` from the docs engine. **src/app/manifest.ts** ```ts import type { MetadataRoute } from "@farm.js/core"; export default function manifest(): MetadataRoute.Manifest { return { name: "Acme Store", short_name: "Acme", description: "The Acme product catalog", start_url: "/", display: "standalone", background_color: "#ffffff", theme_color: "#16a34a", icons: [ { src: "/icon-192.png", sizes: "192x192", type: "image/png" }, { src: "/icon-512.png", sizes: "512x512", type: "image/png" }, ], }; } ``` **src/app/llms.ts** ```ts import type { MetadataRoute } from "@farm.js/core"; export default function llms({ pages, defaults, }: MetadataRoute.LlmsTxtContext): MetadataRoute.LlmsTxt { return { ...defaults, sections: [ { title: "Docs", links: pages.filter((page) => page.path.startsWith("/docs")) }, { title: "Optional", links: [{ title: "Changelog", url: "https://acme.test/changelog" }] }, ], }; } ``` `llms.ts` renders the [llmstxt.org](https://llmstxt.org) format: `title` as the H1, `summary` as a quote, `details` as Markdown, and each section as an H2 list of links. To write the whole file yourself, return it as a string instead; Farm serves it as the complete file and adds a final newline when it is missing. Besides the usual arguments, its function receives `pages`, the app's static pages, each linking to its Markdown mirror when `md` exposes one and to the page itself otherwise, and `defaults`, the index [`agent.llmsTxt`](/docs/configuration#agent-readiness) would serve, so it can extend the generated file instead of starting over. An `llms.ts` replaces `agent.llmsTxt` at its path. `llms-full.ts` serves `/llms-full.txt` the same way. When it returns the structured format, Farm inlines the Markdown mirror of every same-origin link that has one; a string is served as the complete file, with a final newline added when missing. Its function also receives `markdown(url)`, which reads a page's mirror without the request's cookies and returns `null` when the page is unavailable. ```ts // src/app/llms-full.ts: reuse the llms.txt structure and let Farm inline each page export { default } from "./llms"; ``` Farm automatically adds the nearest discovered manifest to rendered page heads unless `metadata.manifest` already supplies an explicit URL. A nested file keeps its route prefix: `src/app/docs/sitemap.ts` is served at `/docs/sitemap.xml`, and a file under `[tenant]` receives the concrete tenant param. With the [docs engine](/docs/docs-engine) enabled, a root `sitemap.ts`, `robots.ts`, `llms.ts`, or `llms-full.ts` takes its path from the docs engine, which keeps serving the ones the app does not define. An app sitemap replaces the docs engine's instead of extending it, so list the docs pages in it when crawlers should find them there. Generated metadata routes accept `GET` and `HEAD` and return `405` for other methods. They revalidate by default. Export `revalidate = 300` for shared CDN caching or `revalidate = false` only for permanently immutable output. A returned `Response` is an escape hatch for custom XML, headers, or status codes. `feed.ts` is not reserved yet because feeds need an explicit RSS, Atom, or JSON Feed contract. Use an API or programmatic route for feeds until that format is defined. ### Alternate representations Advertise other formats of a page, such as that feed route, with `alternates.types`. Each MIME type renders `<link rel="alternate" type href>`, with an optional `title`: ```tsx import type { Metadata } from "@farm.js/core"; export const metadata: Metadata = { alternates: { types: { "application/rss+xml": [{ url: "/blog/feed.xml", title: "Acme blog" }], "application/atom+xml": "/blog/atom.xml", }, }, }; ``` Relative URLs resolve against `metadataBase`, the same as `alternates.languages`. A page that sets `alternates.types` replaces its layout's whole `types` object. When a page has a [Markdown mirror](/docs/markdown), Farm adds `<link rel="alternate" type="text/markdown" href="/blog/hello.md">` to its head next to the `Link` response header it already sends. Set `alternates.types["text/markdown"]` to advertise a different URL. Restricting `md.expose` or setting `md: false` removes the link along with the mirror. ### Static metadata images Place `opengraph-image.png` next to a page or layout segment for a zero-code, route-local social image. Farm supports `.png`, `.jpg`, `.jpeg`, `.gif`, and `.webp` files. ```txt src/app/ opengraph-image.png products/ opengraph-image.png [id]/ page.tsx ``` The root image is the application fallback. `/products/opengraph-image.png` applies to product pages and their descendants, while unrelated routes continue using the root image. Farm automatically reads the image dimensions and content type. Add accessible preview text with an optional sidecar file: **src/app/products/opengraph-image.alt.txt** ```txt Acme product catalog preview ``` Farm serves the file through the extensionless `/products/opengraph-image` endpoint and adds a content fingerprint to the URL emitted in page metadata. Fingerprinted requests receive immutable caching, while direct unversioned requests revalidate with an `ETag`. Use `twitter-image.png` when X/Twitter needs a different image. Otherwise, social platforms can use the Open Graph image. An explicit `metadata.openGraph.images` or `metadata.twitter.images` value always wins over a matching file. Static images inside a dynamic segment are shared by every concrete route. Use a generated image when the preview must depend on route params. ### Generated metadata images Generated JSX metadata images currently use the React image renderer. Every renderer can use static `opengraph-image.png`, `twitter-image.png`, and explicit metadata image URLs. Place `opengraph-image.tsx` or `twitter-image.tsx` next to a route segment when the image needs data or route params. Return ordinary stateless JSX: Farm owns the image endpoint and PNG renderer, so the component does not import `ImageResponse` or declare an API URL. The renderer is `@vercel/og`, which the app installs with `pnpm add @vercel/og`; `farm build` fails with that command when a generated image exists and the package is missing. Farm also adds the nearest matching image to the page head when `openGraph.images` or `twitter.images` is not already set. **src/app/products/[id]/opengraph-image.tsx** ```tsx import type { PageProps } from "@farm.js/core"; export const size = { width: 1200, height: 630 }; export const alt = "Product preview"; export const revalidate = 300; function ProductCard({ name, id }: { name: string; id: string }) { return ( <div className="flex h-full w-full flex-col justify-between bg-[#09090b] p-20 text-white"> <span className="text-3xl font-medium text-emerald-400">Acme</span> <div className="flex flex-col"> <span className="text-2xl text-zinc-400">Product {id}</span> <span className="mt-3 text-8xl font-bold tracking-tight">{name}</span> </div> </div> ); } export default async function ProductOpenGraphImage({ params }: PageProps) { const product = await getProduct(params.id); return <ProductCard id={params.id} name={product.name} />; } ``` For `/products/42`, Farm calls the component with `params.id === "42"`, serves the generated PNG at `/products/42/opengraph-image`, and emits `og:image`, `og:image:width`, `og:image:height`, and `og:image:alt` tags. A nested page such as `/products/42/reviews` inherits this image until a nearer route segment defines its own. The component and its data-loading code remain on the server. `className` supports the image renderer's Tailwind utility set, including arbitrary values. Inline `style` can be used with it and wins when both set the same property. This is not a browser screenshot: image JSX supports flex layout, typography, borders, gradients, absolute positioning, and embedded images, but not CSS Grid, animations, media queries, pseudo-elements, hooks, or stateful and class components. The application stylesheet and custom Tailwind plugins are not executed. Use an absolute URL for an `<img>` source. Farm uses 1200 by 630 when `size` is omitted and includes Geist as the default font. Export `fonts` to embed a custom brand font: ```tsx const brandFont = fetch("https://cdn.example.com/fonts/Brand-Bold.ttf").then((response) => response.arrayBuffer(), ); export const fonts = [ { name: "Brand", data: brandFont, weight: 700 as const, style: "normal" as const }, ]; ``` Satori-compatible TTF, OTF, and WOFF files are supported; WOFF2 is not. Set `fontFamily: "Brand"` on the element that uses the font. Generated images revalidate on every request by default. Export `revalidate = 300` to let a CDN cache the route for five minutes, or `revalidate = false` only when the output is permanently immutable. Farm emits an `ETag`, supports conditional requests and `HEAD`, and performs JSX-to-image rendering internally. For advanced renderers, the default export may still return a `Response`, string, or bytes. To preserve the earlier React-to-SVG behavior, export `contentType = "image/svg+xml"` and return an SVG React element. A returned `Response` keeps its own status, headers, and body. Keep only one implementation for each image kind in a segment. For example, defining both `opengraph-image.png` and `opengraph-image.tsx` produces a build error. For broad social-platform compatibility, use 1200 by 630; generated JSX routes emit PNG automatically. ### Image responses in API routes When an image needs its own URL, such as a share card per record or a badge another site embeds, return `ImageResponse` from an API route. `@farm.js/core/og` exports the same `@vercel/og` renderer that generated metadata images use. Install it once for both with `pnpm add @vercel/og`. **src/app/api/og/route.tsx** ```tsx import { ImageResponse } from "@farm.js/core/og"; export async function GET(request: Request) { const title = new URL(request.url).searchParams.get("title") ?? "Acme"; return new ImageResponse( <div style={{ display: "flex", width: "100%", height: "100%", alignItems: "center", justifyContent: "center", background: "#09090b", color: "white", fontSize: 72, }} > {title} </div>, { width: 1200, height: 630 }, ); } ``` `ImageResponse` is a standard `Response` with a PNG body. Its options take `width`, `height`, `fonts`, `emoji`, and `debug` alongside the usual `status` and `headers`. The element goes to the renderer as written, so style it with `style` or the `tw` prop; the `className` conversion described above applies only to metadata image files. Like generated metadata images, it takes React JSX. Outside development the response defaults to `cache-control: public, immutable, no-transform, max-age=31536000`. Pass a lowercase `cache-control` header when the image can change, for example `headers: { "cache-control": "public, max-age=300" }`. `@farm.js/core/og` is server-only. Importing it from a client component fails the build, and the development request, with an error that names the importing module. Importing it from server code without `@vercel/og` installed fails the same way and names the install command. On Node presets, including `node-server` and Vercel Functions, Farm copies `@vercel/og` and its wasm and font files into the server output whenever the server bundle imports it. Edge and Cloudflare Worker presets are not supported yet. ### Other meta tags Use `other` for a `<meta name>` tag that has no dedicated metadata field. Each key renders one tag, and an array value renders one tag per item: ```tsx import type { Metadata } from "@farm.js/core"; export const metadata: Metadata = { other: { "apple-itunes-app": "app-id=123456789", "format-detection": "telephone=no", }, }; ``` Names and values are escaped. `other` merges one level deep, so a page can replace a key its layout set and keep the rest. Client navigation removes the previous page's `other` tags before adding the next page's. ## File Route States Use `loading.*` and `error.*` next to a file route to define route-local loading and error states. Use `.tsx`/`.jsx` with React, Preact, or Solid, `.vue` with Vue, and `.svelte` with Svelte. Farm picks the nearest matching boundary, so `src/app/dashboard/error.tsx` handles `/dashboard` and nested dashboard pages unless a deeper segment defines its own boundary. ```txt src/app/ dashboard/ page.tsx loading.tsx error.tsx ``` **src/app/dashboard/loading.tsx** ```tsx import type { LoadingProps } from "@farm.js/core"; export default function DashboardLoading(props: LoadingProps) { return <p>Loading {props.path}</p>; } ``` `loading.tsx` is used as the Suspense fallback when the page or nested content suspends while rendering. **src/app/dashboard/error.tsx** ```tsx "use client"; import type { ErrorProps } from "@farm.js/core"; export default function DashboardError({ error, reset }: ErrorProps) { const message = error instanceof Error ? error.message : "Something went wrong"; return ( <section> <h2>Could not load dashboard</h2> <p>{message}</p> <button onClick={reset}>Try again</button> </section> ); } ``` `error.tsx` receives `error`, `reset`, `params`, `path`, `search`, `searchParams`, middleware data, and plugin context. The closest route error boundary handles normal render/data failures. Redirects and `notFound()` still escape to Farm's redirect and not-found handling. ### redirect() and notFound() Import `redirect()`, `permanentRedirect()`, and `notFound()` from `@farm.js/core` or `@farm.js/core/navigation`. They work in pages, layouts, server components, route data hooks, and [middleware](/docs/middleware), and a page that hydrates can import them like any other module. ```tsx import { notFound, type PageProps } from "@farm.js/core"; export default async function ProductPage({ params }: PageProps<"/products/[id]">) { const product = await getProduct(params.id); if (!product) notFound(); return <Product product={product} />; } ``` While the response has not started, Farm answers with the real status: `307` or `308` with a `Location` header, or `404` with the app's configured not-found page. That includes a synchronous page under a `loading.tsx` boundary, and an async page in production, which Farm awaits before rendering. When the call happens after a loading boundary already streamed the `200` shell (a component behind `<Suspense>` that finishes later, or an async page in development), the status can no longer change. Like Next.js, Farm finishes the document so it recovers in the browser: an `http(s)` redirect replaces the location, and `notFound()` swaps the page for the app's not-found page, marks the document `noindex`, and skips hydrating the page that threw. The layouts are already on screen, so the not-found page renders without them; if it needs context a layout provides, Farm shows its built-in not-found page instead. With experimental RSC enabled, failures before the HTML shell is sent render the nearest `error.tsx` through RSC and SSR with status `500` and `Cache-Control: private, no-store`. RSC loading and error boundaries follow the selected page's file ancestors, including route groups and catch-all folders. For example, `/users/[id]/error.tsx` does not handle a sibling `/users/new/page.tsx`. Failures before page selection, such as middleware errors, use only the root error boundary when one exists. This fallback uses a standalone document shell, outside the failed page/layout tree, so a broken layout cannot prevent the error UI from rendering. Client boundaries receive a client-owned `reset()` that reloads the current URL. Production responses show a generic error message; the original exception stays in server logs. Development responses can show the error message. If no boundary exists, or the boundary itself fails during SSR, Farm returns a generic non-cacheable 500 response. Failures after streaming starts cannot change the already-sent status code and remain subject to React's streaming recovery behavior. Place `not-found.*` at the app root to customize unmatched URLs. When the component lives elsewhere, set its project-relative path explicitly; Farm uses the same file in development and production and fails startup or build when the path is missing or incompatible with the selected renderer: ```ts title="farm.config.ts" export default defineConfig({ notFound: { component: "./src/ui/not-found.tsx", }, }); ``` ## Catch-all routes Catch-all routes are useful for docs, CMS content, and nested marketing pages where the page is resolved from content instead of a fixed file for every URL. **src/app/docs/[...slug]/page.tsx** ```tsx import type { PageProps } from "@farm.js/core"; export default function DocsPage({ params }: PageProps<"/docs/[...slug]">) { const slug = params.slug.split("/"); return <main>Docs path: {slug.join(" / ")}</main>; } ``` ## Route groups Use route groups to organize files without adding URL segments. They are useful when an app has multiple shells. **Route groups** ```txt src/app/ (marketing)/ page.tsx pricing/ page.tsx (app)/ dashboard/ page.tsx ``` The group names are organizational. The URLs are still `/`, `/pricing`, and `/dashboard`. ## Pending Navigation UI Use `useNavigation()` for global route-transition UI: top progress bars, disabled navigation buttons, optimistic shells, or app-wide busy indicators. It tracks SPA navigations started by `Link` and `navigateTo`. ```tsx "use client"; import { useNavigation } from "@farm.js/core/client"; export function TopProgress() { const navigation = useNavigation(); return <div data-pending={navigation.pending} aria-hidden={!navigation.pending} />; } ``` `navigation.state` is `"loading"` while route data is being fetched and returns to `"idle"` after the route is committed. `navigation.to` includes the target `pathname`, `search`, `hash`, and full `href`. Use route `loading.tsx` for segment-level Suspense fallbacks, and `useNavigation()` when the surrounding app shell should react to a navigation. ## View Transitions Pass `viewTransition` to `Link` or `navigateTo` when a navigation should use the browser View Transitions API. Farm starts the transition after route data is ready and falls back to normal SPA navigation when the browser does not support it. ```tsx import { Link, navigateTo } from "@farm.js/core/client"; export function GalleryLink() { return ( <Link href="/gallery" viewTransition> Gallery </Link> ); } await navigateTo("/gallery", { viewTransition: true }); ``` Use this for image galleries, dashboards that keep the same app shell, settings panes, and modal-to-page flows. Keep important loading states in `loading.tsx` or `useNavigation()`; view transitions are visual polish, not a loading boundary. ## Navigation workflow 1. Add or rename route files. 2. Keep `farm dev` running; Farm regenerates route and API types when route files change. 3. Use `Link` for internal navigation and plain anchors for external URLs. 4. Keep dynamic route values encoded in the href string, such as `/blog/${slug}`. Run `farm generate` when you want to refresh generated types outside the dev server, such as in CI or after a large file move. --- ## Server Queries URL: /docs/server-queries Define typed server reads once, deduplicate requests, prefetch browser data, use stale-while-revalidate, and invalidate route, API, and query consumers with one key. # Server Queries `createServerQuery` defines a typed server read with one structured cache key. The same declaration works during server rendering, through a generated browser server reference, with browser prefetch, and in `useServerQuery`. The browser lifecycle is intentionally familiar to React Query and TanStack Query users: request deduplication, prefetching, `staleTime`, stale-while-revalidate, focus and reconnect refresh, shared invalidation, and optimistic cache writes. Farm implements this behavior in its own cache and generated server references; it does not install or wrap TanStack Query. > **Browser usage requires the server-function transform** > > The generated browser server references come from the `@farm.js/plugin/rsc` transform, enabled by > adding the plugin and setting `experimental.serverActions: true` in `farm.config.ts`. Without it, > `useServerQuery`, `prefetchServerQuery`, and `fetchServerQuery` can only run during server > rendering: importing a query module from `"use client"` code would bundle the server handler into > the browser, and Farm fails the build with a boundary error instead. In apps without the > transform, call queries from server components, or expose an API route and use > [`apiClient` from `createApiClients()`](/docs/api-client) from client components. ## Declare a query **src/features/products/queries.ts** ```ts import { createServerQuery } from "@farm.js/core/server-query"; import { z } from "zod"; const Product = z.object({ id: z.string(), name: z.string(), price: z.number(), }); export const productQuery = createServerQuery({ input: z.object({ id: z.string() }), output: Product, key: ({ input }) => ["product", input.id], staleTime: "30s", async handler({ input, request, signal, context }) { return db.product.findUniqueOrThrow({ where: { id: input.id }, }); }, }); ``` Farm adds the server-module boundary during compilation. Keep exported `createServerQuery` declarations out of files with `"use client"`, just like `createServerFn` declarations. The `input`, `output`, and middleware contracts are the same as `createServerFn`. Input is validated before the key and handler run. Output is validated before it enters either the server or browser cache. ## Call on the server ```tsx import { productQuery } from "@/features/products/queries"; export default async function ProductPage({ params }: { params: { id: string } }) { const product = await productQuery({ id: params.id }); return <h1>{product.name}</h1>; } ``` Matching calls in one render request share the same in-flight promise. The handler runs once and every caller receives the validated result. ## Prefetch in the browser ```tsx "use client"; import { prefetchServerQuery } from "@farm.js/core/server-query/client"; import { productQuery } from "@/features/products/queries"; export function ProductLink({ id }: { id: string }) { return ( <a href={`/products/${id}`} onPointerEnter={() => prefetchServerQuery(productQuery, { id })}> Open product </a> ); } ``` Concurrent prefetches and mounted consumers use one browser request. A successful prefetch is immediately available to `useServerQuery`. ## Read with browser SWR ```tsx "use client"; import { useServerQuery } from "@farm.js/core/server-query/client"; import { productQuery } from "@/features/products/queries"; export function ProductPrice({ id }: { id: string }) { const product = useServerQuery(productQuery, { id }); if (product.pending) return <ProductPriceSkeleton />; if (product.error && !product.data) return <p>{product.error.message}</p>; return ( <div aria-busy={product.fetching}> <strong>{product.data?.price}</strong> <button type="button" onClick={() => product.refetch()}> Refresh </button> </div> ); } ``` The hook returns `data`, `error`, `status`, `pending`, `fetching`, `stale`, and `refetch`. Stale data remains visible while Farm refreshes it in the background. Stale queries also refresh on window focus and reconnect unless those options are disabled. Set `enabled: false` to pause automatic reads, invalidation refetches, and focus/reconnect refresh. An already-running read is not cancelled. Switching back to `enabled: true` checks the cache again: missing or stale data refreshes, fresh data is reused, and pending work is deduplicated. This does not add polling or continuously refetch when `staleTime` is zero. Explicit `refetch()` remains available while automatic reads are disabled. `refetch()` always starts fresh work. If an older request finishes afterward, its result is returned to its original caller but cannot replace the newer cached value. Invalidating a query while its read is pending keeps that response stale, even if the response timestamp is newer than the invalidation or its canonical key is learned only on arrival. The original caller still receives its result. Enabled mounted consumers refresh after the old work settles; an imperative read with `swr: false` waits for fresh data on the next call. Unrelated invalidations and invalidations before a read starts do not invalidate that read. Broadcast invalidations for keys a client cache does not own are retained only by active request-lifetime trackers, not indefinitely by every cache. Cached, subscribed, and pending keys still receive invalidations, including keys reached through provisional aliases. Use `fetchServerQuery(productQuery, input)` for an imperative browser read that should participate in deduplication and SWR. Calling the generated `productQuery(input)` reference directly still returns plain typed data, but the fetch helper supplies the browser cache lifecycle. With SWR enabled (the default), every imperative reader receives the existing stale value immediately while one shared refresh runs, including readers arriving after that refresh starts. An initial read without cached data still waits. Set `swr: false` to wait for the in-flight result, or `force: true` to start and await a new request instead of joining it. ## Invalidate after a mutation ```ts import { createServerFn } from "@farm.js/core/server-fn"; import { z } from "zod"; export const updateProduct = createServerFn({ input: z.object({ id: z.string(), name: z.string().min(1), }), invalidates: ({ input }) => [{ key: ["product", input.id] }], async handler({ input }) { await db.product.update({ where: { id: input.id }, data: { name: input.name }, }); return { ok: true }; }, }); ``` During a browser server-action call, Farm carries structured invalidations back with the action response. Mounted stale queries refetch automatically, and concurrent consumers still produce one request. If invalidation arrives while a query is still resolving its canonical server key, the invalidation follows that key and the older response cannot make the entry fresh again. If an automatic refresh fails, `useServerQuery` keeps the previous data stale and exposes the error without repeatedly retrying the same invalidation. Call `refetch()` to retry, or let a new invalidation, focus/reconnect event, or re-enabled query trigger another read. Concurrent mounted consumers continue to share that read. ## Share keys with routes and APIs Server queries use the existing route-data cache namespace and tag. They do not create a separate server cache. For a key used in several features, keep its factory in a module that is safe to import from both the browser and server. Plain string and array keys remain the default and require no helper: ```ts export const productKey = (id: string) => ["product", id] as const; ``` Use `defineCacheKey` only when you want TypeScript to carry the value stored under that key into optimistic cache updaters: ```ts import { defineCacheKey } from "@farm.js/core/cache"; import type { Product } from "./types"; export const productKey = defineCacheKey<Product>()((id: string) => ["product", id] as const); ``` The helper adds no wrapper object or runtime cache format. `productKey("123")` is still the raw `["product", "123"]` array, so typed and untyped keys interoperate and existing applications do not need to migrate. Use the same structured key in programmatic route data: ```ts data: { key: ({ params }) => ["product", params.id], staleTime: "30s", async main({ params }) { return getProduct(params.id); }, } ``` Use it in an API client cache when the API response has the same data contract: ```ts const result = await api.products.get( { query: { id } }, { cache: { key: ["product", id], policy: "stale-while-revalidate", staleTime: 30_000, }, }, ); ``` The route, API caller, and server query now read and invalidate the same canonical key. Default API keys remain isolated by request origin; only an explicit structured key opts into cross-feature sharing. When overlapping server-query requests use different function references but resolve to the same canonical key, a late older response cannot replace a newer request's cached result. Each caller still receives its own result. Once both references' keys are known, newer pending and error states are protected too. A key cannot be associated with a previously unknown reference until a Farm transport returns its cache metadata; plain-data transports remain scoped to the function and input. Expired shared-cache entries keep their aliases and invalidation marks while a related subscription or request is active. Releasing an owner rechecks that key without scanning unrelated pending entries. Retargeting an alias also releases the previous target's metadata once it has no owner. ### Share optimistic updates API mutations can optimistically update data watched by `useServerQuery` when both features use the same structured key and the same data shape: ```ts const products = await api.products.get( { query: { category } }, { cache: { key: ["products", category], policy: "stale-while-revalidate", staleTime: 30_000, }, }, ); await api.products.post( { body: draft }, { optimistic: { update: [ [ products.key, (current) => ({ ...current, products: [{ ...draft, id: "optimistic" }, ...(current?.products ?? [])], }), ], ], rollbackOnError: true, }, invalidate: [products.key], }, ); ``` This works through the shared Farm client cache; `useServerQuery` does not expose a separate optimistic option. Keep the API response and server-query result contracts identical whenever they share a key. Server-function mutations reach the same shared cache through `useMutation`: pass key-targeted `request.optimistic` updates and `request.invalidate` targets, as described in [Track mutations in React](/docs/api-client#track-mutations-in-react). ## Cache lifetime | `staleTime` | Server behavior | Browser behavior | | ----------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------ | | omitted or `0` | Deduplicate within the current request without retaining the result | Treat a result as immediately stale and refresh it when read again | | duration such as `"30s"` or `30000` | Store in the shared route-data cache until stale | Return fresh data until the duration expires, then use SWR | | `false` | Store until explicit invalidation | Keep fresh until explicit invalidation | Numbers are milliseconds. Duration strings support `ms`, `s`, `m`, and `h`. Failed handlers and invalid output are never cached. When the app configures a [client cache adapter](/docs/api-client#persist-the-client-cache), declare `persist: true` on a query to allow its results to be stored on the device and rendered stale-but-visible on the next cold start. Nothing persists without the flag; treat it as part of the query's security review, since persisted results outlive the session. ## Middleware and cancellation Queries accept the same composable middleware as server functions: ```ts export const accountQuery = createServerQuery({ middleware: [requireSession], input: z.object({ accountId: z.string() }), key: ({ input, context }) => ["account", context.user.id, input.accountId], async handler({ input, context, signal }) { return getAccount(input.accountId, context.user.id, { signal }); }, }); ``` Authentication middleware still runs for every query invocation, including cache hits. The request signal aborts when the underlying server-action request is cancelled. ## Security practices - Treat a server-query reference as transport, not authorization. Verify authentication, role, tenant, and resource ownership on the server. - Include every identity that changes the result in a persistent key. For private data, use keys such as `["account", context.user.id, accountId]`, not only `["account", accountId]`. - Prefer request-only caching by omitting `staleTime` when a safe shared persistent key is not available. - Keep output schemas narrow so private database fields cannot enter the browser cache accidentally. - Never place secrets, tokens, or raw session objects in keys; keys can appear in diagnostics and invalidation metadata. - Use API routes instead of server queries for intentionally cross-origin or public HTTP contracts. ## Production practices - Keep keys small, deterministic, and serializable. - Use one key for one data shape. Route data, API responses, and queries should share a key only when their cached value contracts match. - Prefetch on strong intent such as pointer focus, viewport proximity, or an immediately likely next step. - Render stale data with a quiet `fetching` state instead of replacing useful content with a full loading screen. - Invalidate immediately after the database transaction succeeds. - Return typed expected states such as `notFound` or `forbidden`; reserve thrown errors for unexpected failures. --- ## Rendering Model URL: /docs/server-rendering Choose dynamic rendering, static rendering, ISR, or PPR with route-level exports and config. # Rendering Model Choose dynamic rendering, static rendering, ISR, or PPR with route-level exports and config. Rendering controls decide when HTML is produced. [Route Runtime](/docs/route-runtime) separately decides where a dynamic page or API handler executes and how deployment limits are applied. Dynamic rendering, static rendering, and revalidation apply to every renderer. The component examples below use React-compatible TSX; see [Renderers](/docs/renderers) for Preact, Solid, Vue, and Svelte conventions and the features that remain React-specific. ## Rendering options RSC page URLs return HTML for document visits and a Flight payload for requests accepting `text/x-component`. Both representations include `Vary: Accept`. When you enable shared caching, configure the CDN or reverse proxy to honor `Vary`; do not cache these responses by URL alone. Farm preserves existing `Vary` fields, including `Origin` and Nitro's `Accept-Encoding`. | Mode | How to opt in | Best for | | ------- | ---------------------------------------------- | --------------------------------------- | | Dynamic | Default for request-bound pages | Dashboards and personalized UI. | | Static | dynamic = force-static or use static directive | Marketing pages and stable docs. | | ISR | revalidate = seconds | Content that can refresh on a schedule. | | PPR | experimental_ppr = true | Static shells with dynamic holes. | ## Route-level config **src/app/pricing/page.tsx** ```tsx export const dynamic = "force-static"; export const revalidate = 300; export default async function PricingPage() { return <main>Pricing</main>; } ``` ## Use directives when compactness wins Farm also recognizes compact rendering directives at the top of route modules. This keeps small examples readable while preserving explicit exports for Next-style compatibility. **src/app/blog/page.tsx** ```tsx "use ssg; 60"; export default function BlogPage() { return <main>Blog</main>; } ``` `use ssg; 60` statically generates the route and revalidates it every 60 seconds. Farm also accepts `use ssg`, `use dynamic`, and `use ppr; 60` when those rendering modes fit the route. ## Dynamic rendering Use dynamic rendering for request-specific pages such as dashboards, account settings, and pages that depend on cookies, headers, or per-user data. With experimental React Server Components enabled, Farm streams HTML while inserting the styles, hydration payload, and client script. HTML injection preserves UTF-8 bytes across stream chunks, including emoji and non-ASCII content, without buffering the entire page. **src/app/dashboard/page.tsx** ```tsx export const dynamic = "force-dynamic"; export default async function DashboardPage() { return <main>Dashboard</main>; } ``` ## Static rendering Use static rendering for pages that can be built once and served quickly. During a production build, Farm also scans routes that use the default dynamic mode. If a non-parameterized route does not read cookies, headers, authentication, search parameters, or middleware request data, Farm reports it as a static-rendering candidate. The build keeps the route dynamic until you opt in, because request-bound work may be hidden in an imported component or data loader. Review each suggestion, including its imported code, then add the static export when it is safe. This keeps personalized pages dynamic while making stable pages easy to move onto the fastest rendering and caching path. If an explicitly static route directly reads request APIs or request props, Farm reports the unsafe read and keeps the route dynamic instead of freezing request-specific HTML into a shared artifact. This check covers the route module itself; continue reviewing imported components and data loaders, because request-bound work in transitive imports cannot be proven safe from the route source alone. Farm serves generated HTML only for `GET` and `HEAD` requests. Other request methods continue through the live route instead of receiving a cached page document. **src/app/about/page.tsx** ```tsx export const dynamic = "force-static"; export default function AboutPage() { return <main>About Farm</main>; } ``` ## ISR-style revalidation `revalidate` caches a static response and refreshes it after the configured number of seconds. **src/app/pricing/page.tsx** ```tsx export const dynamic = "force-static"; export const revalidate = 300; export default async function PricingPage() { const plans = await loadPlans(); return <PricingTable plans={plans} />; } ``` ## PPR with Suspense PPR is for pages with a stable shell and dynamic sections. Put dynamic work behind Suspense boundaries so the shell can be cached while the slower section resolves independently. **src/app/dashboard/page.tsx** ```tsx import { Suspense } from "react"; export const experimental_ppr = true; export const revalidate = 60; export default function DashboardPage() { return ( <main> <h1>Dashboard</h1> <Suspense fallback={<RevenueSkeleton />}> <RevenuePanel /> </Suspense> </main> ); } ``` ## Deferred route islands The route-level hydration strategy is shared by renderer adapters. Preact uses the same nested `"use client"` analysis through its compatibility runtime; Solid, Vue, and Svelte currently hydrate at the route boundary selected by their adapter. Client routes and server routes that import a client boundary can defer their JavaScript while Farm keeps the server-rendered HTML visible. Add an optional static `island` export to the client module: ```tsx "use client"; export const island = "interaction"; export function CopyButton({ value }: { value: string }) { return <button onClick={() => navigator.clipboard.writeText(value)}>Copy</button>; } ``` Farm propagates the strategy to the route hydration boundary, emits it in the route manifest, and splits production route modules behind dynamic imports. Deferred routes therefore avoid evaluating their route chunk until its trigger while preserving the initial SSR output. These strategies control hydration of the initial server-rendered document. During client-side navigation, the navigation itself signals user intent, so Farm loads and renders a route-wide destination immediately instead of leaving the previous route visible while waiting for another trigger. | Strategy | Hydration trigger | | ------------- | --------------------------------------------------------------------- | | `load` | Immediately. This is the default and compatibility-first behavior. | | `interaction` | The first button-like click; Farm replays that click after hydration. | | `visible` | When the route boundary approaches the viewport. | | `idle` | During browser idle time, with a timeout fallback. | With [isolated client hydration](/docs/configuration#isolated-client-hydration), each eligible client module keeps its own strategy, including after SPA navigation. Sibling boundaries can mix all four strategies: one boundary's trigger never hydrates another, and an interaction click is claimed and replayed once by the boundary that contains it. Removing a boundary cancels its pending observer, idle callback, or interaction listener. Farm uses the same isolated boundary metadata for Vite development, streamed or buffered SSR, and statically generated HTML. In development, updating a client module rerenders every live boundary created from that module without importing or replacing its server-owned layout; sibling boundary state stays mounted. If one page needs the route-wide fallback, that fallback stays on the page boundary and does not promote otherwise eligible client leaves in its server-owned layout. Farm also keeps graphs above four statically bounded isolated roots route-wide. Data-dependent boundary lists use the same fallback because their root count is unknown before streaming. This measured guard prevents request, marker, root, and hydration overhead from growing past the first observed crossover; the [benchmark report](https://github.com/farming-labs/farm.js/blob/main/benchmarks/isolated-hydration/results/latest.md) includes route-wide, isolated, and RSC controls with raw samples. The representative fixtures also run route-wide and isolated hydration with Farm's experimental React compiler enabled, proving both the initial hydration cost and repeated state-update cost of the combined path. The export must be one of these static string literals so Farm can analyze it without executing application code. Without an explicit route-level `island` export, a route that imports client boundaries with different strategies safely falls back to `load` because its current route-level React-compatible root cannot schedule those children independently. Keep interactive leaves small today; a eligible leaves can also use Farm's experimental React compiler for direct state binding updates after their independent roots mount. ### Async pages and client components A page or layout can be an `async` server component and still render `"use client"` components that work in the browser: ```tsx import { StarButton } from "./star-button"; export default async function RepoPage() { const repo = await getRepo(); return ( <main> <h1>{repo.name}</h1> <StarButton initialCount={repo.stars} /> </main> ); } ``` React cannot run an `async` component in the browser, so the page itself stays server-rendered and never ships to the client. Each eligible client boundary it renders hydrates as its own island, with the props the server passed it. This works by default in React apps; it does not need `experimental.isolatedClientHydration`. Under a parent layout that hydrates as a whole route, the layout keeps the async page as server HTML and the islands inside it still hydrate, including after client navigation between pages of that layout. Navigating away cancels pending island work and unmounts its roots without waiting for hydration. React's early-update diagnostic during that synchronous cancellation is expected; hydration mismatches, updates to live roots, and other errors are still reported. Islands have the same limits as [isolated client leaves](#isolated-client-leaves-without-rsc): - Props must be serializable. Passing server-rendered JSX as `children` or another prop (`<ClientShell><ServerList /></ClientShell>`) does not cross the boundary. - A client component must use a supported export shape and must not sit where the HTML parser moves it, such as directly inside `<table>`. - An integration provider that wraps the whole route, and does not declare `supportsIsolatedHydration: true`, keeps its routes route-wide. When one of these applies, the route stays server-rendered, its client components are not interactive, and Farm logs a warning naming the module and the reason. Fix the reason, or render the interactive part from a synchronous page. Preact, Solid, Vue, and Svelte keep async pages server-only. Client components that come from a package, such as the icons in `lucide-react`, cannot be isolated yet. An async page renders them as static HTML and still hydrates its own client components as islands, and Farm logs which packages it left static. ### Isolated client leaves without RSC `"use client"` also remains available in a normal React SSR application. Set `experimental.isolatedClientHydration` to `"analyze"` to audit which local client leaves Farm can split without changing runtime behavior, or to `"enabled"` to hydrate safe leaves independently. Unsupported routes keep the existing route-wide ownership model. See [Isolated client hydration](/docs/configuration#isolated-client-hydration) for the modes and safety rules. This experiment is disabled when RSC owns the route. Integration providers also keep the route-wide root unless they explicitly declare that they can be recreated around independent isolated roots. ## Automatic optimized boundaries Automatic optimized boundaries are a React-only experiment and are not applied to Preact, Solid, Vue, or Svelte routes. Farm can experimentally render large, non-interactive Server Component regions through the native Strata renderer. Enable the flag once in `farm.config.ts`: ```ts import { defineConfig } from "@farm.js/core"; export default defineConfig({ experimental: { serverComponents: true, optimizedBoundary: true, }, }); ``` Application components remain ordinary JSX: ```tsx export default function ArticlePage() { return ( <article className="prose"> <h1>Representation-aware rendering</h1> <p>Farm selects this host-only region automatically.</p> </article> ); } ``` Farm evaluates eligible host-element trees on the server and uses Strata only when the region is large enough to benefit. Trees containing Client Components, event handlers, refs, unsupported elements or attributes, unsafe URLs, or other uncertain values retain normal React rendering. The flag does not require an application boundary component or a direct Strata dependency. The current Strata runtime uses a native Node binding. Keep this experiment on Node deployments until a Wasm or JavaScript fallback is available for edge and Cloudflare worker targets. ## Choosing a mode | Need | Use | | -------------------------------------- | --------------------------------------- | | User-specific data on every request | `dynamic = "force-dynamic"` | | Stable docs, marketing, or policy page | `dynamic = "force-static"` | | Stable page with scheduled refresh | `revalidate = 60` | | Static shell plus dynamic holes | `experimental_ppr = true` with Suspense | --- ## Stability and Support URL: /docs/stability What Farm 0.1 treats as stable, beta, and experimental, which renderers and deployment targets are verified, and how breaking changes are introduced. # Stability and Support Farm 0.1 is the first release line with a compatibility promise. This page states what the promise covers, how each package, renderer, deployment target, and feature is classified, and how changes are introduced. ## Stability levels | Level | What you can rely on | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Stable** | Covered by the versioning policy below. Breaking changes follow a deprecation in an earlier minor release. | | **Beta** | Supported and tested, but the API can still change in a minor release without a deprecation cycle. Beta packages keep publishing `0.1.0-beta.N` versions. | | **Experimental** | Opt-in only, behind an `experimental` option or an explicit flag. It can change or be removed in any release. Disabled experimental features add no runtime cost. | ## Versioning policy Farm follows semantic versioning with the usual rules for `0.x` releases: - **Patch releases** (`0.1.1`, `0.1.2`) fix bugs and security issues. They do not remove or change stable APIs, configuration, generated route types, or production output contracts. - **Minor releases** (`0.2.0`) may change stable APIs. A stable API is deprecated in an earlier minor release first, keeps working with a runtime or type-level warning, and the release notes include migration steps. - **Experimental APIs** can change in any release. The release notes call out those changes. - Security fixes may tighten behavior in a patch release when the previous behavior was unsafe. The release notes say so explicitly. The integration packages pin `@farm.js/core` to the exact version they were released with, so keep every package in the shared release group on the same version. `farm upgrade` does this for you. Independently versioned beta plugins must also be republished against a stable core before a stable release is promoted. The release publisher checks registry manifests and stops if a beta package still resolves a prerelease core, preventing two Farm runtimes from being installed. ## Packages **Stable, released together at the same version:** - `@farm.js/core`, `@farm.js/cli`, `@farm.js/create-app`, `@farm.js/plugin`, `@farm.js/devtools` - `@farm.js/integration-utils` and the `@farm.js/integrations` compatibility re-exports - The first-party integrations: `@farm.js/ai`, `@farm.js/auth`, `@farm.js/auth0`, `@farm.js/authjs`, `@farm.js/autumn`, `@farm.js/better-auth`, `@farm.js/cf-agent`, `@farm.js/clerk`, `@farm.js/contentful`, `@farm.js/email`, `@farm.js/eve`, `@farm.js/jobs`, `@farm.js/polar`, `@farm.js/preview-gateway`, `@farm.js/sanity`, `@farm.js/sentry`, `@farm.js/strapi`, `@farm.js/stripe`, `@farm.js/supabase`, `@farm.js/unkey`, `@farm.js/workos` An integration's stable surface is its Farm API: the factory, options, mounted routes, generated client bindings, and documented request and response shapes. Provider behavior follows the provider SDK version declared in the package's peer dependencies. **Beta, versioned independently:** - Renderer packages: `@farm.js/react` (the React compiler and runtime package; the default React path lives in core), `@farm.js/preact`, `@farm.js/solid`, `@farm.js/vue`, `@farm.js/svelte` - Plugins: `@farm.js/analyzer`, `@farm.js/cache-redis`, `@farm.js/content`, `@farm.js/hints`, `@farm.js/msw`, `@farm.js/otel`, `@farm.js/partytown`, `@farm.js/preview-tunnel`, `@farm.js/pwa`, `@farm.js/scripts`, `@farm.js/search`, `@farm.js/stylex`, `@farm.js/sync` **Experimental packages:** `@farm.js/federation`, `@farm.js/mcp`, `@farm.js/wasm`, `@farm.js/webmcp`. ## Renderers | Renderer | Level | Package | Notes | | -------- | ------ | -------------------------- | --------------------------------------------------------------------- | | React | Stable | built into `@farm.js/core` | Default renderer. Used by every template unless you pick another one. | | Preact | Beta | `@farm.js/preact` | | | Solid | Beta | `@farm.js/solid` | | | Vue | Beta | `@farm.js/vue` | | | Svelte | Beta | `@farm.js/svelte` | Buffered server rendering (no streaming). | Framework-level behavior (routing, APIs, actions, queries, middleware, deployment) is renderer-neutral and covered by the shared renderer conformance suite. See [Renderers](/docs/renderers) for the per-renderer capability matrix. ### Experimental React compiler The compiler is experimental. Its package is independently versioned and stays beta until its first non-prerelease version is published. Its application API is `react({ compiler: true })`; the former `experimental.compiler` spelling is a deprecated compatibility alias. The compiler is disabled by default, and unsupported components retain the normal React path. The stable-release contract covers the documented compiler options and `@farm.js/react/list`. Generated compiler definitions and their runtime must use the same package version: rebuild both server and browser output when upgrading. The compiler's internal representation and the exact set of optimized components are not a cross-version API. See the [compiler compatibility guide](/docs/renderers/react#compatibility-and-release-status). ## Deployment targets | Target | Preset | Level | How it is verified | | ---------------------------- | ------------------ | ------------ | ---------------------------------------------------------------------------------------------- | | `node` | `node-server` | Stable | Production browser suites in CI run against a built `node-server` output. | | `vercel` | `vercel` | Stable | Build Output tests in CI; the Farm documentation site is deployed with it. | | `cloudflare` | `cloudflare-pages` | Stable | Built output runs in `workerd` on every supported Node.js version in CI. | | `netlify` | `netlify` | Stable | Built output runs through Netlify's request pipeline on every supported Node.js version in CI. | | Direct Nitro `preset` values | any | Pass-through | Farm passes the preset to Nitro. Output is not tested by Farm unless it appears above. | `deploy.target` values get Farm's defaults and deploy commands. A raw Nitro `preset` such as `vercel-edge`, `netlify-edge`, `cloudflare-module`, `deno`, or `bun` is supported on a best-effort basis. See [Deployment](/docs/deployment). ## Experimental features These are off by default and outside the stability promise: | Feature | How to enable | | ------------------------- | ------------------------------------------------------------------- | | React Server Components | `experimental.serverComponents`, or the `@farm.js/plugin/rsc` setup | | Server Actions under RSC | `experimental.serverActions` | | Partial Prerendering | `experimental.ppr`, then opt routes in | | Isolated client hydration | `experimental.isolatedClientHydration: "analyze" \| "enabled"` | | API route MCP server | `apiMcp()` from `@farm.js/mcp` | | WebMCP browser tools | `@farm.js/webmcp` | | Module federation | `@farm.js/federation` | | WebAssembly components | `@farm.js/wasm` | ## Known limits in 0.1 - **Runtime-only docs features on edge.** Farm can precompile the built-in renderer and external adapters that publish an edge compiler, but adapter features that require live server callbacks or mutable sessions still need a Node target. The official adapter reports the exact unsupported features during the build. - **Node.js 22.13 or newer** is required for development and for Node deployment targets. ## Reporting problems Open an issue at [github.com/farming-labs/farm.js](https://github.com/farming-labs/farm.js/issues) with the Farm version (`farm doctor` prints it), the renderer, and the deployment target. --- ## KV Storage URL: /docs/storage Use Farm's key/value API for caches, settings, counters, idempotency records, and object-backed values. # KV Storage Farm's `@farm.js/core/storage` module is a **key/value API**. Every value is stored under a string key and read with operations such as `getItem`, `setItem`, `getKeys`, and `removeItem`. Use it for caches, feature flags, application settings, idempotency records, checkpoints, and object-backed values. Do not use it as a relational ORM for users, accounts, products, or other records that need model fields, relations, joins, or typed filters. Request-rate enforcement uses a separate atomic counter contract described below. ## Choose the right data API | What you need | Use | | ------------------------------------------------- | ----------------------------------------------------------- | | Cache entries, flags, settings, counters, or JSON | Farm KV storage through `getStorage(name)` | | Rate limiting shared across production instances | An atomic adapter such as `redisRateLimitStorage(...)` | | Files or values addressed by one key | An object-backed KV helper such as `s3Storage(...)` | | Application models, relations, joins, and filters | An application-owned `@farming-labs/orm` or another ORM | | Models owned by a schema-backed Farm integration | Farm's integration ORM through `ctx.args.db` | | Better Auth users, accounts, and sessions | Better Auth's configured database adapter and instance APIs | | Provider-specific SQL or database operations | The raw integration runtime client through `getClient()` | The current beta config groups two different inputs under the `storage` key: - `storage.driver`, `storage.mounts`, and a Farm storage client configure the KV system used by `getStorage()`. - A raw database or ORM object passed to `storage.client` is reserved for schema-backed integrations and is documented under [Database and ORM Clients](/docs/integrations/orm-storage). These paths do not convert into each other. In particular, a raw PostgreSQL pool supplied as `storage.client` does not become the value returned by `getStorage()`, and it does not make the default in-memory KV store durable. ## Create a KV client **src/lib/storage.ts** ```ts import { sqliteStorage } from "@farm.js/core/storage"; export const appStorage = sqliteStorage({ path: "./.farm/storage/app.sqlite", tableName: "app_store", }); ``` KV helpers return ready-to-use clients, so application code can import and call them directly. Mounting the clients is useful when you want one central configuration and a stable name that any server-side module can retrieve later. ## Register named mounts **farm.config.ts** ```ts import { defineConfig } from "@farm.js/core"; import { appStorage } from "./src/lib/storage"; export default defineConfig({ storage: { mounts: { app: appStorage, }, }, }); ``` Each property under `mounts` is a KV namespace: - `app` is an application-defined name. Farm does not attach special behavior to it. - You can add names such as `cache`, `sessions`, `uploads`, or `webhooks` when the application needs more stores. A mount name is not a URL, route, database table, or filesystem directory. It is the lookup name that connects configuration to later server-side calls. `getStorage(name)` always returns a namespaced key/value view. When the name matches a configured mount, operations use that mount's driver. When no matching mount exists, Farm uses the same namespace on the root store instead. The default root store is in memory, so a missing or misspelled production mount does not throw but may store data only for the lifetime of one process. Rate-limit middleware is an exception: enforcement requires its dedicated atomic storage contract and never treats a generic KV mount as atomic. ## Use a mounted store Call `getStorage("app")` from server-side code to retrieve the client registered under the `app` mount. It is not limited to API routes. You can call it from any module that executes on the server after Farm initializes storage. Prefer resolving the store inside the request handler, action, job, or lifecycle function that uses it so the configured storage is ready before lookup. **src/app/api/settings/route.ts** ```ts import { createEndpoint } from "@farm.js/core/api"; import { getStorage } from "@farm.js/core/storage"; import { z } from "zod"; type AppSettings = { theme: "light" | "dark"; productName: string; }; const settingsSchema = z.object({ theme: z.enum(["light", "dark"]), productName: z.string().min(1), }); const defaultSettings: AppSettings = { theme: "dark", productName: "Farm.js App", }; export const GET = createEndpoint("/api/settings", { method: "GET" }, async () => { const appStore = getStorage("app"); const settings = await appStore.getItem<AppSettings>("settings"); return { settings: settings ?? defaultSettings, }; }); export const POST = createEndpoint( "/api/settings", { method: "POST", body: settingsSchema, }, async (ctx) => { const appStore = getStorage("app"); await appStore.setItem("settings", ctx.body); return { saved: true, settings: ctx.body, }; }, ); ``` The same mount can be used from other server-only application surfaces: | Surface | Example use | | ----------------------------------- | -------------------------------------------------------------------------------- | | API routes | Persist settings, idempotency keys, webhook state, or cached provider responses. | | Middleware | Read feature flags, request policy, tenant configuration, or custom counters. | | Server components and pages | Load data needed during server rendering. | | Server actions and server functions | Save form state or invalidate application-owned cached values. | | Integration server handlers | Read application key/value data that is separate from an integration schema. | | Jobs and workflows | Store checkpoints, deduplication markers, or lightweight progress state. | Do not call `getStorage()` from a client component or browser bundle. Expose the required operation through an API route, server action, or server function instead. ## KV operations Mounted stores expose the standard Farm KV API: ```ts const appStore = getStorage("app"); await appStore.setItem("feature:checkout", { enabled: true }); const feature = await appStore.getItem<{ enabled: boolean }>("feature:checkout"); const exists = await appStore.hasItem("feature:checkout"); const keys = await appStore.getKeys("feature:"); await appStore.removeItem("feature:checkout"); await appStore.clear(); ``` `clear()` only clears the selected namespace. Calling `getStorage("app").clear()` does not clear `ratelimit`, `cache`, or another mounted store. Use descriptive keys such as `settings:global`, `tenant:acme:flags`, or `webhook:event_123`. Namespaced keys make inspection and targeted cleanup easier. ## Atomic rate-limit storage The built-in rate limiter uses an atomic `increment(key, windowMs)` contract. Its default adapter is atomic inside one process, which is useful for local development and a single long-running server. Production deployments with multiple processes or regions should pass a shared atomic adapter explicitly: ```bash pnpm add @farm.js/cache-redis ioredis ``` **src/app/api/middleware.ts** ```ts import { redisRateLimitStorage } from "@farm.js/cache-redis"; import { middleware } from "@farm.js/core/middleware"; import Redis from "ioredis"; const rateLimits = redisRateLimitStorage({ client: () => new Redis(process.env.REDIS_URL!), prefix: "storefront-ratelimit", }); export default middleware().rateLimit({ requests: 100, window: "1m", storage: rateLimits, }); ``` The default bucket is the caller's address and the request path. A built app takes that address from the server's own connection, as the development server does. Behind a reverse proxy or a platform load balancer that connection belongs to the proxy, so set `server.trustProxy` to key on the proxy's `x-forwarded-for` instead. When the runtime reports no address at all, the limiter refuses rather than putting every caller in one bucket, so set `trustProxy` or supply a `keyGenerator` that uses an identity you do have. Do not fall back to a constant such as `"unknown"`: that is the shared bucket the limiter refuses, and one caller then exhausts the window for everyone. The Redis adapter uses one Lua operation to increment the counter and establish its expiry. A generic `get()` followed by `set()` adapter is rejected because concurrent requests can read the same count and overwrite each other. Limited responses include `Retry-After`; all responses include the [`RateLimit` and `RateLimit-Policy` fields from the current IETF draft](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/). Choose a trusted rate-limit identity when possible. An authenticated user or tenant ID is usually more useful than an IP address for application-level limits: ```ts export default middleware().rateLimit({ requests: 20, window: "1m", keyGenerator: (ctx) => { const userId = ctx.data.get("userId") as string | undefined; if (userId) return `user:${userId}`; const address = ctx.request.socket?.remoteAddress; // No identity and no address means this request cannot be bucketed on its // own, so reject it rather than sharing a bucket with every other caller. if (!address) throw new Error("Rate limit identity is unavailable for this request."); return `ip:${address}`; }, }); ``` Adapters may implement `get(key)` so `getRateLimitStatus()` can inspect a counter. Enforcement itself only depends on atomic `increment()`. `memoryRateLimitStorage({ maxEntries })` caps active keys (100,000 by default) and rejects a new key when that capacity is full; it never evicts an active counter to admit another key. Expired windows are reclaimed when needed, and repeated capacity rejections skip cleanup scans until a window could expire. Its `increment()` and `get()` results are snapshots: editing a returned counter does not change the stored count or expiry. Use a shared adapter for enforcement across processes or regions. ## Choosing mounts and drivers Mount names describe the responsibility; drivers decide where the values live: | Use case | Suggested mount | Typical driver | | ------------------------------------------- | ---------------------------- | ----------------------------------------------------- | | Application settings and durable JSON state | `app` or `settings` | SQLite, Postgres, MySQL, or libSQL | | Shared cache entries | `cache` | Redis, Upstash Redis, or memory for local development | | Rate-limit counters | Dedicated rate-limit adapter | `redisRateLimitStorage(...)` or another atomic store | | Idempotency and webhook deduplication | `webhooks` or `idempotency` | Redis or a durable SQL store | | Bucket-backed objects or metadata | `uploads` | S3 or Vercel Blob | | Tests and disposable local state | Any descriptive name | Memory or local filesystem | The mount names are conventions chosen by the application. Two mounts may use the same driver type while remaining isolated, or use different drivers based on durability and latency requirements. A mount named `ratelimit` is still generic KV storage and is not accepted as proof of atomic increment support. ## Supported KV drivers Farm supports KV storage at three levels: 1. Farm convenience helpers for common databases, caches, and object stores. 2. Direct driver configuration through `driver: "name"`. 3. Existing or custom `unstorage` driver instances through `databaseStorage()`, `driverStorage()`, or `defineStorageClient()`. The root store and every mount can use a different supported driver. ### Farm KV helpers Import these helpers from `@farm.js/core/storage`: | Helper | Driver | Common use | | --------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------- | | `memoryStorage()` | `memory` | Tests, local development, and disposable process-local state. | | `localStorage({ base })` | Farm alias for `fs-lite` | Local persistent files, development caches, and self-hosted single-instance apps. | | `sqliteStorage({ path, tableName })` | `sqlite` / `node-sqlite` | Durable local application state with no external service. | | `postgresStorage(...)` / `pgStorage(...)` | `postgres` | Shared durable key/value state backed by Postgres. | | `mysqlStorage(...)` / `mysql2Storage(...)` | `mysql` | Shared durable key/value state backed by MySQL. | | `pgliteStorage(...)` | `pglite` | Embedded Postgres-compatible storage. | | `planetscaleStorage(...)` | `planetscale` | PlanetScale-backed durable storage. | | `libsqlStorage(...)` | `libsql` | Local or remote libSQL/Turso-compatible storage. | | `redisStorage(...)` | `redis` | Shared caches, sessions, counters, and short-lived state. | | `upstashStorage(...)` | `upstash` | HTTP-based Redis storage for serverless and edge-style deployments. | | `mongodbStorage(...)` | `mongodb` | Durable document-backed key/value storage. | | `s3Storage(...)` | `s3` | S3-compatible object-backed values. | | `netlifyBlobsStorage(...)` | `netlify-blobs` | Named or deploy-scoped Netlify Blob stores. | | `vercelKVStorage(...)` | `vercel-kv` | Vercel KV/Redis-backed shared state. | | `vercelBlobStorage(...)` | `vercel-blob` | Public Vercel Blob-backed values. | | `createStorageClient({ driver, ...options })` | Any supported name | Create a reusable client from direct driver configuration. | | `databaseStorage(database, { tableName })` | `db0` | Reuse an existing `db0` database instance. | | `driverStorage(driver)` | Custom | Wrap an existing `unstorage` driver or driver factory. | | `defineStorageClient(factory)` | Custom | Lazily create a custom synchronous or asynchronous driver. | ### Configure drivers directly Helpers and direct configuration produce the same Farm storage client behavior. Driver options can be written inline or inside `options`. If both are present, values inside `options` take precedence. **farm.config.ts** ```ts import { defineConfig } from "@farm.js/core"; export default defineConfig({ storage: { driver: "sqlite", path: "./.farm/storage/root.sqlite", tableName: "farm_root", mounts: { cache: { driver: "redis", url: process.env.REDIS_URL!, ttl: 300, }, uploads: { driver: "s3", endpoint: "https://s3.us-east-1.amazonaws.com", region: "us-east-1", bucket: process.env.S3_BUCKET!, accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, }, }, }, }); ``` In this example, `getStorage()` uses SQLite, `getStorage("cache")` uses Redis, and `getStorage("uploads")` uses S3. ### Database-backed KV drivers Farm resolves these names through `db0` and exposes the result as key/value storage: | Accepted driver names | Database connector | Important configuration | | ------------------------------ | ----------------------- | ----------------------------------------------------------------- | | `sqlite`, `node-sqlite` | Node SQLite | `path` or `name`, plus optional `tableName`. | | `sqlite3` | `sqlite3` | `path` or `name`, plus optional `tableName`. | | `better-sqlite3` | `better-sqlite3` | `path` or `name`, plus optional `tableName`. | | `postgres`, `postgresql`, `pg` | PostgreSQL | `url` or standard `pg` client options, plus optional `tableName`. | | `mysql`, `mysql2` | MySQL | Standard `mysql2` connection options, plus optional `tableName`. | | `pglite` | PGlite | PGlite connector options, plus optional `tableName`. | | `planetscale` | PlanetScale | PlanetScale client options, plus optional `tableName`. | | `libsql`, `libsql-node` | libSQL Node client | `url`, optional `authToken`, and optional `tableName`. | | `libsql-http` | libSQL HTTP client | `url`, optional `authToken`, and optional `tableName`. | | `db0` | Existing `db0` database | Pass `{ database, tableName? }` inside `options`. | These drivers create or use a key/value table for Farm KV storage. They do not expose tables, models, joins, or arbitrary SQL through `getStorage()`. Passing a raw database or ORM object through `storage.client` is a separate integration database path. ### Complete built-in driver set Farm also accepts the built-in driver names exported by its installed `unstorage` version. Kebab-case and camel-case names shown on the same row are aliases. | Category | Accepted driver names | Intended use | | ----------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------- | | Process memory | `memory` | Fast, process-local, non-durable state. | | Disabled storage | `null` | Discard writes and always read empty state. Useful for explicitly disabling a store. | | Layered storage | `overlay` | Combine multiple driver layers. Requires configured driver instances in `layers`. | | Bounded memory | `lru-cache`, `lruCache` | Process-local cache with size and TTL controls. | | Filesystem | `local`, `fs-lite`, `fsLite`, `fs` | Persistent files. `local` is Farm's shorthand for `fs-lite`; `fs` adds watcher support. | | Remote HTTP | `http` | Read and write through an HTTP storage endpoint. | | GitHub content | `github` | Read-only, cached repository content access. | | Redis | `redis` | Shared cache, counters, sessions, and short-lived state. | | Upstash Redis | `upstash` | REST-based Redis for serverless runtimes. | | MongoDB | `mongodb` | MongoDB collection-backed values. | | S3-compatible objects | `s3` | AWS S3, Cloudflare R2's S3 API, and compatible providers. | | UploadThing | `uploadthing` | UploadThing-backed object values. | | Netlify | `netlify-blobs`, `netlifyBlobs` | Named or deploy-scoped Netlify Blob storage. | | Vercel KV | `vercel-kv`, `vercelKV` | Vercel-managed Redis-compatible key/value storage. | | Vercel Blob | `vercel-blob`, `vercelBlob` | Vercel Blob object storage. | | Vercel Runtime Cache | `vercel-runtime-cache`, `vercelRuntimeCache` | Ephemeral regional runtime cache with TTL and tags. | | Cloudflare KV binding | `cloudflare-kv-binding`, `cloudflareKVBinding` | A KV namespace bound to a Cloudflare runtime. | | Cloudflare KV HTTP | `cloudflare-kv-http`, `cloudflareKVHttp` | Cloudflare KV through the REST API. | | Cloudflare R2 binding | `cloudflare-r2-binding`, `cloudflareR2Binding` | An R2 bucket bound to a Cloudflare runtime. | | Azure App Configuration | `azure-app-configuration`, `azureAppConfiguration` | Azure App Configuration key/value data. | | Azure Cosmos DB | `azure-cosmos`, `azureCosmos` | Cosmos DB container-backed values. | | Azure Key Vault | `azure-key-vault`, `azureKeyVault` | Secret-backed values in Azure Key Vault. | | Azure Blob Storage | `azure-storage-blob`, `azureStorageBlob` | Azure container-backed object values. | | Azure Table Storage | `azure-storage-table`, `azureStorageTable` | Azure table-backed key/value state. | | Deno KV | `deno-kv`, `denoKV` | Deno runtime KV storage. | | Deno KV from Node | `deno-kv-node`, `denoKVNode` | Deno KV through the Node-compatible client. | | IndexedDB | `indexedb` | IndexedDB-backed values in a compatible browser runtime. | | Web Storage | `localstorage`, `session-storage`, `sessionStorage` | Browser local or session storage. | | Capacitor | `capacitor-preferences`, `capacitorPreferences` | Capacitor Preferences-backed mobile storage. | Farm KV storage normally initializes on the server. Browser-only drivers require a compatible custom runtime and should not be used as a reason to call `getStorage()` from client components. Most remote and platform drivers load an optional provider SDK. Install the package required by the selected driver, such as `ioredis`, `mongodb`, `@upstash/redis`, `@vercel/kv`, `@vercel/blob`, `@netlify/blobs`, `aws4fetch`, `uploadthing`, the relevant Azure SDK, `@deno/kv`, `idb-keyval`, or `lru-cache`. Database aliases may likewise require `sqlite3`, `better-sqlite3`, `mysql2`, `@electric-sql/pglite`, `@planetscale/database`, or `@libsql/client`; the `sqlite` alias uses Node's built-in `node:sqlite`. Cloudflare binding drivers instead require the corresponding runtime binding. ### Custom drivers Use `driverStorage()` when a compatible driver already exists: ```ts import { driverStorage } from "@farm.js/core/storage"; import createCustomDriver from "my-unstorage-driver"; export const customStorage = driverStorage(() => createCustomDriver({ endpoint: process.env.CUSTOM_STORAGE_URL!, }), ); ``` The wrapped driver can be mounted or used as the root Farm storage client just like a built-in helper. ## Database and ORM clients are separate The current beta API uses `storage.client` for two distinguishable object shapes: - A Farm storage client, such as `sqliteStorage(...)`, becomes the root key/value store used by `getStorage()`. - A raw database, ORM, or provider object becomes the runtime client for schema-backed integrations. That object is not returned by `getStorage(name)`. ```ts import { defineConfig } from "@farm.js/core"; import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync("farm.sqlite"); export default defineConfig({ storage: { client: db, }, }); ``` In this example, the raw SQLite database is available only to integrations. Because no KV driver or Farm storage client is configured, `getStorage()` continues to use the default in-memory KV store. When an integration also defines a schema, Farm exposes a typed ORM layer at `ctx.args.db`. The integration does not need to know whether the app passed SQLite, PostgreSQL, or another supported runtime client. Use this rule of thumb: - Use `getStorage("name")` for key/value operations against a named namespace or mount. - Use `ctx.args.db` inside an integration that declares models and needs ORM-style queries. - Use `ctx.args.storage.getClient()` inside integration server code only when provider-specific operations require the raw configured runtime client. See [Database and ORM Clients](/docs/integrations/orm-storage) for application-owned ORM usage, integration schemas, PostgreSQL pools, Better Auth ownership, and migrations. ## KV client or database client | Config | Use it for | | ------------------------------------------- | ------------------------------------------------------ | | `sqliteStorage(...)` | Farm KV storage with a Farm storage client. | | `redisStorage(...)` | Cache or queue-like key/value data. | | `storage.mounts` | Multiple named key/value stores. | | `storage.client` with a Farm storage client | Reuse a created Farm storage client as the root store. | | `storage.client` with a DB/runtime object | Give integrations a runtime database client. | ## Production notes - Use a shared durable KV driver for production state that must survive restarts or be visible across instances. - Keep memory KV storage for tests and explicitly disposable local state. - Do not assume that a PostgreSQL-backed KV helper provides relational database access. - Configure KV mounts and database clients independently when an application needs both. --- ## Product telemetry URL: /docs/telemetry Understand and control Farm.js CLI and production-site telemetry. # Product telemetry Farm.js has two separately controlled product-telemetry paths: - Anonymous command telemetry in `@farm.js/cli` (`farm`) and `@farm.js/create-app` is **enabled by default** for interactive local commands. - Production website reporting is **enabled by default** in built server runtimes and automatically reduces incoming production request URLs to their public HTTPS origin. These signals help the maintainers understand which coarse framework paths are useful, where compatibility work should be focused, and which websites are running Farm in production. This is separate from [application observability](/docs/observability). OpenTelemetry describes what your application does and is configured by the application owner. Farm.js product telemetry never includes application spans, logs, visitor analytics, or request contents. ## Control CLI telemetry ```bash farm telemetry status farm telemetry enable farm telemetry disable ``` The first eligible event creates a random anonymous installation ID in the operating system's local configuration directory. `farm telemetry disable` opts out and deletes that ID. Running `farm telemetry enable` later creates a different ID. Saved opt-out preferences remain respected across upgrades. Environment variables can provide an explicit per-process or organization-wide policy: | Variable | Behavior | | --------------------------- | ----------------------------------------------------------------------- | | `FARM_TELEMETRY=1` | Enables CLI telemetry, including non-interactive and CI commands. | | `FARM_TELEMETRY=0` | Disables CLI and production-site telemetry for the process. | | `FARM_TELEMETRY_DISABLED=1` | Disables CLI and production-site telemetry for the process. | | `DO_NOT_TRACK=1` | Disables all telemetry and takes precedence over Farm's enable setting. | | `FARM_TELEMETRY_DEBUG=1` | Prints delivery status without event contents, origins, or identifiers. | Without the explicit `FARM_TELEMETRY=1` override, Farm skips test, CI, and non-interactive processes even when the saved local preference is enabled. The preference file is stored at: - macOS: `~/Library/Application Support/farmjs/telemetry.json` - Linux: `$XDG_CONFIG_HOME/farmjs/telemetry.json`, or `~/.config/farmjs/telemetry.json` - Windows: `%APPDATA%\farmjs\telemetry.json` ## Automatic production-site detection No site URL configuration is required. After the first non-health request reaches a built Farm server runtime, Farm reads the request URL and reduces it to an origin. For example, `https://shop.example.com/private/orders?token=secret` becomes only `https://shop.example.com` before telemetry delivery. Farm accepts detected origins only when they use HTTPS and a public hostname. HTTP, localhost, IP, single-label, and `.local` origins are ignored. Request paths, query strings, hashes, and credentials are discarded and never included in the check-in. To disable production-site reporting in `farm.config.ts`: ```ts title="farm.config.ts" import { defineConfig } from "@farm.js/core"; export default defineConfig({ telemetry: false, }); ``` Vercel preview, development, and custom-environment deployments are skipped automatically from `VERCEL_ENV` and `VERCEL_TARGET_ENV`, as are Netlify preview, branch, and dev deployments (when `NETLIFY=true`) and Render pull-request previews (when `IS_PULL_REQUEST=true`). Farm does not classify deployments from the hostname suffix, so a production website whose public domain ends in `.vercel.app` remains eligible. On a provider Farm does not auto-detect, set `FARM_TELEMETRY=0` or `FARM_TELEMETRY_DISABLED=1` in preview environments while leaving production enabled. The Farm-owned endpoint removes the Vercel-confirmed legacy preview aliases that were stored before this runtime guard existed. It does not guess whether a deployment is a preview from its hostname. After the first non-health production request, Farm schedules a check-in through the deployment runtime's background-work hook. It does not wait for the network before handling or returning the application response. A running instance checks in at most once every 24 hours, and a failed request is eligible for a later best-effort retry. Multiple instances update the same site record. The check-in contains only the detected origin, `@farm.js/core` version, renderer name, and deploy target. It does not contain a visitor or installation identifier, the full request URL beyond the reported origin, headers, cookies, IP address, user-agent string, or application data. Fully static exports have no server runtime and therefore do not send production-site check-ins. Vercel preview, development, and custom-environment deployments do not send production-site check-ins. Set `telemetry: false` and redeploy to stop future check-ins. An inactive site disappears from the maintainer dashboard after the retention window. ## Data that is sent Farm currently sends three versioned event types: | Event | Fields | | ------------------------ | ------------------------------------------------------------------------------------------------------- | | `command_invoked` | Allowlisted command from `farm` or `create-farm-app`, package/version, optional deploy target, runtime. | | `project_created` | Allowlisted starter, renderer, package manager, TypeScript/install booleans, runtime fields. | | `production_site_active` | Automatically detected HTTPS origin, core version, renderer, and deploy target. | The `farm` binary records each actionable command path, including nested commands such as `auth:migrate`, `cron:list`, `cron:run`, and `add:integration`. The app generator records `create` or `list-templates`, and a completed scaffold also records `project_created`. Help, version, and the `farm telemetry` privacy-control commands do not emit events. Every CLI event also has a random event ID for deduplication and the random local installation ID. The server immediately converts the installation ID into an HMAC hash using a server-only salt; the raw ID is not stored. Production-site check-ins have neither identifier and are upserted by the detected origin. A telemetry-enabled production server also answers `/.well-known/farm-telemetry` with the same framework metadata so the ingestion service can attest the claimed origin before storage. Receipt time is assigned by the server instead of trusting a client timestamp. Farm does **not** collect or store: - project names, filesystem paths, Git remotes, repository names, source code, or route names; - usernames, email addresses, account IDs, cookies, application payloads, or application events; - environment variable names or values, database URLs, credentials, tokens, or other secrets; - IP addresses or user-agent strings. The CLI client schedules delivery in the background so telemetry does not delay command execution, and network delivery does not keep a short-lived process open. A request has a three-second timeout and transient network, rate-limit, and server failures receive two bounded retries while the process remains alive. Telemetry can never make a Farm command fail, and there is no persistent retry queue. ## Endpoint, validation, and retention CLI events are posted to `https://farmjs.dev/api/telemetry/v1/events`; production-site check-ins are posted to `https://farmjs.dev/api/telemetry/v1/sites`. Both endpoints accept a strict, versioned JSON schema, reject unknown fields and bodies larger than 8 KiB, and rate-limit traffic. CLI events are deduplicated by event ID. Before a production site is upserted, the service fetches its fixed well-known endpoint through a private-address-blocking connection, rejects redirects and oversized responses, and stores the metadata returned by that origin instead of client-claimed metadata. Rows created before origin attestation was introduced remain conservatively labeled as unverified and disappear through the normal retention window unless a current deployment attests them. Raw telemetry events and inactive production-site records are retained for 90 days by default and are pruned by the ingestion service. The verified legacy Vercel preview records are also removed during this maintenance pass. Aggregated package-download counts remain available independently through npm's public download statistics. A deployment operator can change the retention window with `FARM_TELEMETRY_RETENTION_DAYS`. For local endpoint development only, `FARM_TELEMETRY_ENDPOINT` and `FARM_TELEMETRY_SITE_ENDPOINT` can point at an HTTPS URL or an HTTP localhost address. Released clients use the Farm-owned endpoints by default. An invalid or insecure explicit override is skipped instead of being redirected to a Farm-owned endpoint. ## Maintainer deployment setup The Farm-owned docs deployment uses four server-only environment variables: | Variable | Purpose | | -------------------------------- | -------------------------------------------------------------------- | | `DATABASE_URL` | Pooled Postgres connection used by Prisma. | | `FARM_TELEMETRY_IDENTITY_SALT` | Long random secret used to HMAC-hash local anonymous IDs. | | `FARM_TELEMETRY_DASHBOARD_TOKEN` | Long random secret used to open the internal `/telemetry` dashboard. | | `FARM_TELEMETRY_RETENTION_DAYS` | Optional event and inactive-site retention window; defaults to `90`. | These values must be encrypted deployment variables and must never use a `PUBLIC_` prefix or be committed to the repository. Public telemetry clients do not contain an ingestion secret; the endpoints use strict validation, body limits, rate limits, event deduplication, and origin attestation instead. After connecting Postgres, generate the Prisma client and apply the schema from the repository: ```bash pnpm --dir docs prisma:generate pnpm --dir docs db:push ``` `/telemetry` exchanges the dashboard token through a server-side form for a 12-hour HttpOnly, SameSite cookie. The token is not placed in the URL, local storage, or client-side JavaScript. --- ## Testing URL: /docs/testing Test Farm requests, programmatic routes, API endpoints, and server functions with the real framework runtimes. # Testing Farm provides runner-agnostic helpers from `@farm.js/core/testing`. They use Web `Request` and `Response` objects and call the same route data, API endpoint, and server-function runtimes used by the application. ## Create a harness Set request defaults once for a test file. The `context` callback has the same input and behavior as `context` in `farm.config.ts`. ```ts import { afterEach } from "vitest"; import { createFarmTestHarness } from "@farm.js/core/testing"; import { getSession } from "../src/session"; export const farm = createFarmTestHarness({ origin: "https://app.example.test", headers: { "x-tenant": "acme" }, cookies: { session: "test-session" }, context: async ({ request }) => ({ session: await getSession(request), }), }); afterEach(() => { farm.clearCache(); }); ``` Harness defaults are merged with each call. Per-call headers and cookies win when the same name is provided. ## Test a route Pass the exported `createRoute` value directly. Farm builds the URL, validates params and search, resolves request context, runs the guard, then runs `data.before`, `data.main`, and `data.after` in production order. ```ts import { expect, test } from "vitest"; import { ProductRoute } from "../src/features/products/page"; import { farm } from "./farm"; test("loads a product route", async () => { const result = await farm.route(ProductRoute, { params: { id: "product-1" }, search: { tab: "reviews" }, }); expect(result.request.url).toBe("https://app.example.test/products/product-1?tab=reviews"); expect(result.props.data.product.id).toBe("product-1"); expect(result.props.search.tab).toBe("reviews"); }); ``` `result.props` preserves the route's inferred params, search, and `data.main` result types. It is the safe prop object passed to the page component: private app context is not included. `result.element` is a React element for the route component, while `result.canonicalPath` exposes search cleanup such as stripped defaults or temporary params. Guards keep their normal control flow, so redirects and not-found signals can be asserted directly: ```ts await expect( farm.route(DashboardRoute, { context: { session: { user: null } }, }), ).rejects.toMatchObject({ digest: "FARM_REDIRECT;307;/login", }); ``` Pass `context` to one route call to override the harness context factory. Use `pluginContext` or `middleware` when the component also needs request-scoped plugin or middleware data. ## Test API routes Use `api` for a programmatic API route. Dynamic params are interpolated and then matched again by the real API matcher. Endpoint schemas and response normalization still run. ```ts import { expect, test } from "vitest"; import { ProjectApi } from "../src/features/projects/api"; import { farm } from "./farm"; test("updates a project", async () => { const response = await farm.api(ProjectApi, { method: "PATCH", params: { id: "project-1" }, query: { source: "form" }, json: { name: "Farm" }, }); expect(response.status).toBe(200); await expect(response.json()).resolves.toMatchObject({ id: "project-1", name: "Farm", }); }); ``` Use `endpoint` for a handler exported by a file route: ```ts import { GET } from "../src/app/api/projects/[id]/route"; const response = await farm.endpoint(GET, { path: "/api/projects/project-1", params: { id: "project-1" }, }); ``` A file route takes its method from the export name, so `endpoint` uses the `method` you pass, then the endpoint's own `method`, then `POST` when the request has `json`, `form`, or `body`, and `GET` otherwise. The helpers return JSON 404 and 405 responses just like the API route manager. Thrown handlers become a 500 response by default. Set `throwOnError: true` when a unit test needs to inspect the original error. ## Test server functions `serverFn` keeps input and return inference, including Zod validation and `FormData` conversion. It also installs the request and cancellation signal that the handler reads. Any `createServerMiddleware` dependencies run through the same chain as production, including their typed context, ordering, and errors. ```ts import { expect, test } from "vitest"; import { signup } from "../src/actions/signup"; import { farm } from "./farm"; test("signs up a user", async () => { const result = await farm.serverFn(signup, { email: "ada@example.com", password: "correct horse battery staple", }); expect(result.ok).toBe(true); }); ``` Pass a `FormData` value to test form actions, or provide an exact request when the handler reads authorization headers or cookies: ```ts const request = farm.request("/actions/signup", { method: "POST", headers: { authorization: "Bearer test-token" }, }); await farm.serverFn(signup, formData, { request }); ``` Use an aborted signal to test cancellation: ```ts const controller = new AbortController(); controller.abort(new Error("cancelled")); await expect(farm.serverFn(signup, input, { signal: controller.signal })).rejects.toThrow( "cancelled", ); ``` ## Build requests Use `createTestRequest` without a harness, or `farm.request` when you want harness defaults. ```ts import { createTestRequest } from "@farm.js/core/testing"; const request = createTestRequest("/api/search", { origin: "https://app.example.test", query: { q: "routing", tag: ["farm", "react"] }, cookies: { preview: "enabled" }, json: { limit: 10 }, }); ``` Provide only one of `json`, `form`, or `body`. A body defaults the method to `POST`; explicit `GET` and `HEAD` requests reject non-empty bodies. ## Testing boundaries - Clear the route data cache in `afterEach` when tests use `data.key`. The cache is process-wide, matching the application runtime. - Keep database, storage, email, and provider clients behind context or module boundaries so tests can supply controlled implementations. - Use `farm.route` for route contracts and hook flow. Use a React renderer when the assertion concerns component interactions or DOM output. - Use `farm.serverFn` for input validation, handler authorization, form conversion, request access, and cancellation. - Add an HTTP E2E test for server-action transport security. Direct server-function tests do not exercise Origin/Host checks, Fetch Metadata, action-reference decoding, or payload-size limits. - Assert both success and expected failures. A typed result such as `{ ok: false, reason: "forbidden" }` should be tested separately from unexpected thrown errors. --- ## Themes URL: /docs/themes Configure light, dark, and system color modes with a pre-paint selector, Tailwind variants, and typed client and server APIs. # Themes FARMJS can manage a visitor's light, dark, or system preference without owning your colors. The framework applies the active mode before styles load, persists the preference, and exposes typed client and server APIs. Tailwind utilities, CSS variables, or ordinary selectors still define how the application looks. ## Enable themes Add `theme` to the application config: **farm.config.ts** ```ts import { defineConfig } from "@farm.js/core"; export default defineConfig({ theme: { default: "system", storageKey: "farm-theme", }, }); ``` `default` accepts `"light"`, `"dark"`, or `"system"`. Theme support is opt-in; omit the property or set it to `false` when the application manages color mode itself. FARMJS stores the preference in a same-site cookie so server rendering can read it. It mirrors changes to local storage for cross-tab updates. The cookie path follows the configured `basePath`. ## Tailwind dark variants When the built-in Tailwind integration processes a stylesheet containing `@import "tailwindcss"`, FARMJS connects the `dark:` variant to its `data-theme` selector automatically. The integration runs when the app installs `tailwindcss` and `@tailwindcss/vite` (`pnpm add -D tailwindcss @tailwindcss/vite`) and has no PostCSS config of its own: ```tsx export function Panel() { return ( <section className="bg-white text-neutral-950 dark:bg-black dark:text-white"> Theme-aware content </section> ); } ``` The active document is either `<html data-theme="light">` or `<html data-theme="dark">`. If the stylesheet already defines `@custom-variant dark`, FARMJS preserves that definition instead of overriding it. ## Plain CSS and design tokens Tailwind is optional. Use the same selector with CSS variables or ordinary styles: ```css :root { --background: #ffffff; --foreground: #0a0a0a; } [data-theme="dark"] { --background: #000000; --foreground: #f5f5f5; } body { background: var(--background); color: var(--foreground); } ``` FARMJS also sets `color-scheme` for the resolved mode so native form controls and browser surfaces match the page. ## Read and change the theme Use `useTheme` in a client component: ```tsx "use client"; import { useTheme } from "@farm.js/core/theme/client"; export function ThemePicker() { const { theme, resolvedTheme, mounted, setTheme } = useTheme(); return ( <div role="group" aria-label="Color theme"> {(["light", "dark", "system"] as const).map((option) => ( <button key={option} type="button" aria-pressed={theme === option} onClick={() => setTheme(option)} > {option} </button> ))} <span aria-live="polite"> {mounted && resolvedTheme ? `Using ${resolvedTheme} mode` : "Resolving theme"} </span> </div> ); } ``` The returned values have different jobs: - `theme` is the saved `"light"`, `"dark"`, or `"system"` preference. - `resolvedTheme` is the active `"light"` or `"dark"` browser mode. It is undefined during server rendering when the saved preference is `"system"`. - `mounted` becomes true when the browser runtime is active. - `setTheme(theme)` saves and applies a preference. - `toggleTheme()` switches between the resolved light and dark modes. The client module also exports non-hook `getTheme`, `setTheme`, and `toggleTheme` functions for event handlers or stores outside React components. ## Read the preference on the server Server-rendered pages and helpers can read the cookie-backed preference: ```tsx import { getTheme } from "@farm.js/core/theme/server"; export default function SettingsPage() { const theme = getTheme(); return <p>Saved preference: {theme}</p>; } ``` `getTheme()` returns the preference, not an invented server-side resolution for `"system"`; only the browser knows the visitor's operating-system color mode. Prefer CSS and `useTheme` for visual styling. When server-rendered content depends on the cookie, treat that route as request-specific rather than shared static output. ## No-flash behavior FARMJS places a small bootstrap script and color-scheme style at the start of the document head. They resolve the cookie and operating-system preference before application CSS loads. The runtime also follows operating-system changes while `theme === "system"` and preserves `data-theme` during SPA document navigation. ## Configuration reference | Option | Type | Default | Purpose | | ------------ | ------------------------------- | -------------- | --------------------------------------------- | | `default` | `"light" \| "dark" \| "system"` | `"system"` | Preference used before a visitor chooses one. | | `storageKey` | `string` | `"farm-theme"` | Cookie and cross-tab storage key. | --- ## Upgrading to 0.1 URL: /docs/upgrading Move a Farm app from the 0.1.0 betas to the stable 0.1 release: upgrade commands, behavior changes, and deprecated APIs to replace before 0.2. # Upgrading to 0.1 Farm 0.1.0 is the first stable release. It follows `0.1.0-beta.108` and carries the [stability policy](/docs/stability): patch releases keep stable APIs working, and a minor release removes a stable API only after deprecating it first. ## Upgrade the packages Run the upgrade command from the app root: ```bash farm upgrade --latest ``` It moves every published `@farm.js/*` dependency to its `latest` tag with the app's package manager. Packages in the shared release group, such as `@farm.js/core`, `@farm.js/cli`, `@farm.js/devtools`, and the integrations, move to `0.1.0`. Renderer packages and plugins that are still in beta move to their newest beta, which is what their `latest` tag points to. Preview the commands first with `farm upgrade --latest --dry-run`. To upgrade by hand, drop the `@beta` tag and keep every shared-group package on the same version. The integration packages pin `@farm.js/core` exactly, so mixing versions produces peer conflicts: ```bash pnpm add @farm.js/core@latest pnpm add -D @farm.js/cli@latest @farm.js/devtools@latest ``` pnpm 10.16 and newer can hold back versions published in the last day through `minimumReleaseAge`. Templates add `@farm.js/*` to `minimumReleaseAgeExclude`; add the same entry to your `pnpm-workspace.yaml` if an upgrade right after a release resolves an older version. ## Behavior changes These changes landed between the last beta and 0.1.0. Most apps need no code change. - **React streaming on Node targets.** With React 19, production servers on Node presets now stream through `renderToPipeableStream`, the same primitive development uses. Earlier betas used React's Web stream on Node when it was available. Response bodies are unchanged. - **React on edge targets.** Edge presets bundle React DOM's edge server build (`server.browser` on React 18) instead of its Node build. Cloudflare module Workers now bundle React instead of failing at startup with `No such module "react"`. - **Content Security Policy nonces and hashes.** Enable `security.csp.nonce` for a strict script policy. Farm generates a per-request nonce for dynamic HTML and applies it to streamed script elements. Fully prerendered pages stay static and receive per-page SHA-256 hashes instead. See [Content Security Policy](/docs/configuration#content-security-policy). - **Caller-owned databases.** `storage.dispose()` no longer closes a database you passed to `databaseStorage(database)`. Databases Farm creates from storage configuration are still closed by Farm. - **DevTools version.** `@farm.js/devtools` is now released with core and shares its version. ## Feature packages the app installs Starting with 0.1.22, `@farm.js/core` no longer installs the packages behind optional features. An app that uses one of these features declares the package itself. New starters already do, and `farm doctor` reports a feature that is in use without its package. | Feature | Install | | ---------------------------------------------------------- | ------------------------------- | | Built-in Tailwind CSS (a stylesheet imports `tailwindcss`) | `pnpm add -D @tailwindcss/vite` | | `opengraph-image`/`twitter-image` JSX, `@farm.js/core/og` | `pnpm add @vercel/og` | | Image optimization on Node, Vercel, Netlify, and in dev | `pnpm add sharp` | | Docs engine (`docs` enabled, or a `docs.config.*` file) | `pnpm add @farming-labs/docs` | Without `@tailwindcss/vite`, Farm keeps building without Tailwind and logs the install command the first time a stylesheet imports `tailwindcss`. Without `@vercel/og`, `farm build` fails with the install command when the app has a generated metadata image or imports `@farm.js/core/og`; static `opengraph-image.png` files do not need it. Without `sharp`, `images.provider: "auto"` serves original image URLs (the documented `provider: "none"` behavior) and logs the install command, and `images.provider: "node"` fails. Without `@farming-labs/docs`, `farm dev` and `farm build` fail with the install command when docs are enabled. ## Deprecated APIs to replace before 0.2 These still work in 0.1 and are scheduled for removal in a later minor release. ### Plugin lifecycle hooks | Deprecated | Use instead | | --------------------------------------------------------------- | -------------------------------- | | `init` | `setup` | | `config` | `configure` | | `ready` | `runtime.start` | | `shutdown` | `runtime.close` | | `devServerCreated` | `dev.server` | | `hmrUpdate` | `dev.update` | | `buildStart`, `beforeBundle` | `build.before` | | `buildEnd`, `afterBundle` | `build.after` | | `beforeNitroBuild` | `build.configure` | | `routeDiscovered`, `middlewareDiscovered`, `apiRouteDiscovered` | `router.discovered` | | `routesGenerated` | `router.generated` | | `beforeRouteMatch` / `afterRouteMatch` | `router.before` / `router.after` | | `beforeRender` | `render.before` | | `afterRender`, `transformHTML` | `render.html` | | `transformPage` | `render.before` or `render.html` | | `beforeApiHandler`, `beforeRequest` | `runtime.before` | | `afterApiHandler`, `afterResponse` | `runtime.after` | | `requestContext` on the plugin context | `ctx.req` | ### Integrations and schemas | Deprecated | Use instead | | -------------------------------------------------------- | ------------------------------------- | | `slot` on integration descriptors, `FarmIntegrationSlot` | `category`, `FarmIntegrationCategory` | | `requestContext`, `FarmIntegrationRequestContextStore` | `req`, `FarmRequestStore` | | `defineIntegrationSchema` | `defineSchema` (an exact alias) | | `FarmIntegrationSchema*` types | the matching `FarmSchema*` types | ### Configuration and routes | Deprecated | Use instead | | -------------------------------------------- | ---------------------------------------------------- | | The built-in DevTools dashboard (`devtools`) | `devtools()` from `@farm.js/devtools` in `plugins` | | `defineCron()` | `cron` in `farm.config.ts`, pointing at an API route | | `generateStaticParams` | `getStaticPaths` | | `md.routes` | `md.expose` | | `security.contentSecurityPolicy` | `security.csp` | ## Getting help If an app behaves differently after upgrading, run `farm doctor` and open an issue with its output, the renderer, and the deployment target. ---