# 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. ## 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 `farm generate`, development startup, and production builds emit `apiRoutes` alongside `APIRouter` in `src/lib/api.generated.ts`. 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. ## 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( { method: "POST", body: z.object({ name: z.string().min(1), }), }, async ({ body }) => { return Response.json({ message: "Hello " + body.name }); }, ); ``` 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( { method: "GET", 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( { method: "GET", 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( { method: "POST", 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( { method: "PATCH", 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( { method: "POST", 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( { method: "PATCH", 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 preview | Create a public URL for a running local app. | | 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 .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]`, and adds a default export for simple `component: ComponentName` route files. 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. 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. ## 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. ## 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 | Opt-in agent readiness: llms.txt, schema.org JSON-LD, and AI crawler rules. | | 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. | | 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. | ## 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 opt-in features that help AI agents and crawlers understand a public site, and decide which of them may use it. All of them are off by default, so internal tools and private dashboards are unaffected. ```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" }, }, }); ``` `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. `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, so set `revalidate` (seconds) to let a CDN cache both generated files on busy sites. 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: true` adds a schema.org `Organization` 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. 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 ` ``` Keep ordinary templates, ` {@render children?.()} ``` Keep ordinary Svelte markup, runes, stores, actions, transitions, and scoped styles. Replace SvelteKit imports and conventions inside each component, and move FARMJS route exports such as `metadata` and `hydrate` into ` ``` Then define and forward the method used by application code: ```ts export const track = defineForward<(event: string, properties?: Record) => 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) => 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 (

You are offline

Reconnect to load fresh server data.

); } ``` 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 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 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 ` | 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. ``` ### It adds, and never changes or drops 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 the person who knows which one it was, so take it to 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. ### 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 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 ` 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 }; 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 "__idx"` | | `constraints: [...]` | `CREATE [UNIQUE] INDEX "
__"` | | `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 ` {@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 ``` 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 ``` 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 ``` 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 ` ``` `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 ``` 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 ``` The endpoint can call a validated `createServerFn`; its handler, database access, and secrets remain in the server bundle. ## 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 ; } ``` `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
{children}
; } ``` 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 ; } ``` 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 (
{children} {modal}
); } ``` `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
User: {params.id}
; } ``` 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` 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. 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 ( <> About Routing ); } ``` ## 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 {router.params.id}; } ``` 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
{/* ... */}; } ``` 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 ( <> {page?.modal === "cart" ? : 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("docs-sidebar"); return
{/* links */}
; } ``` 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 ( ); } ``` 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 ( ); ``` 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 (

{data.product.name}

}>
); } function Reviews({ reviews }: { reviews: Deferred }) { const resolvedReviews = use(reviews); return resolvedReviews.map((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`, 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>; 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
Product
; } ``` 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) { 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 `` 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 `` 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 `` 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 { 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 `` 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 `` 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 ``, 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 `` 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. 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 (
Acme
Product {id} {name}
); } export default async function ProductOpenGraphImage({ params }: PageProps) { const product = await getProduct(params.id); return ; } ``` 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 `` 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, so the app does not install it separately. **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(
{title}
, { 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. 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 `` 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

Loading {props.path}

; } ``` `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 (

Could not load dashboard

{message}

); } ``` `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 ; } ``` 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 `` 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
Docs path: {slug.join(" / ")}
; } ``` ## 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
; } ``` `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 ( Gallery ); } 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

{product.name}

; } ``` 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 ( prefetchServerQuery(productQuery, { id })}> Open product ); } ``` 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 ; if (product.error && !product.data) return

{product.error.message}

; return (
{product.data?.price}
); } ``` 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()((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
Pricing
; } ``` ## 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
Blog
; } ``` `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
Dashboard
; } ``` ## 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
About Farm
; } ``` ## 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 ; } ``` ## 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 (

Dashboard

}>
); } ``` ## 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 ; } ``` 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 (

{repo.name}

); } ``` 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 (``) 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 `
`. - 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 (

Representation-aware rendering

Farm selects this host-only region automatically.

); } ``` 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. ## 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"` | | React compiler | `react({ experimental: { compiler: true } })` from `@farm.js/react` | | 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("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()`. ## 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" }, }); ``` 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: ```tsx export function Panel() { return (
Theme-aware content
); } ``` The active document is either `` or ``. 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 (
{(["light", "dark", "system"] as const).map((option) => ( ))} {mounted && resolvedTheme ? `Using ${resolvedTheme} mode` : "Resolving theme"}
); } ``` 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

Saved preference: {theme}

; } ``` `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. ## 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. ---